docs: add .agents workspace and the Astro refactor plan
Adds the agent-facing workspace and a 20-task plan for migrating the site to Astro. Nothing here implements the refactor; these are briefs, rules and templates that the task agents read. - .agents/ holds context, rules, checklists, skills, specialist agents, component/page/config templates and gate scripts. It is vendor-neutral so MiniMax, Gemini and Codex can all read it; CLAUDE.md just points at AGENTS.md. - .husky/ plus .lintstagedrc.json wire the three gate tiers. gate.sh locks on the shared git-common-dir so parallel worktrees serialise, and guards the assertion count in scripts/verify.mjs against a coverage drop. - plans/astro-refactor/ carries the phase graph, per-task briefs and the model-routing recommendation. These files must be tracked before fanning out: a worktree only checks out tracked files, so an untracked plan is invisible to every agent working in one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# Orchestrator
|
||||
|
||||
Map of `.agents/`, and how to pick who does what.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.agents/
|
||||
ORCHESTRATOR.md you are here
|
||||
context/ how things are — read before deciding
|
||||
rules/ how things must be — binding
|
||||
checklists/ gates to run before you claim done
|
||||
skills/ procedures an agent loads on demand
|
||||
agents/ specialist agent definitions
|
||||
templates/ component and page starting points
|
||||
scripts/ deterministic helpers (prefer these over prose)
|
||||
```
|
||||
|
||||
`context/` describes reality, `rules/` constrain change. When they disagree,
|
||||
reality won — update the rule and say so.
|
||||
|
||||
## Specialists
|
||||
|
||||
Each agent has one responsibility, one set of rules, and its own worktree.
|
||||
Full definitions in [`agents/`](agents/).
|
||||
|
||||
| Agent | Owns | Loads skills |
|
||||
| --- | --- | --- |
|
||||
| [`astro-architect`](agents/astro-architect.md) | project scaffold, config, routing, layouts | `astro-page` |
|
||||
| [`design-system-keeper`](agents/design-system-keeper.md) | tokens, the single palette, type scale | `design-tokens` |
|
||||
| [`component-builder`](agents/component-builder.md) | one component per task, from templates | `astro-component`, `design-tokens` |
|
||||
| [`page-migrator`](agents/page-migrator.md) | one page per task, HTML → `.astro` | `astro-page`, `content-migration` |
|
||||
| [`motion-designer`](agents/motion-designer.md) | transitions, islands with animation | `motion` |
|
||||
| [`content-i18n-migrator`](agents/content-i18n-migrator.md) | strings out of `app.js` into content collections | `content-migration` |
|
||||
| [`verification-engineer`](agents/verification-engineer.md) | keeping `verify.mjs` meaningful across the refactor | `verify-contract`, `visual-regression` |
|
||||
| [`reviewer`](agents/reviewer.md) | merge gate; reads diffs, never writes features | all |
|
||||
|
||||
## Working agreement
|
||||
|
||||
1. **One task, one worktree, one agent.** See [`rules/git-worktrees.md`](rules/git-worktrees.md).
|
||||
2. **Read `context/` first.** Especially `design-system.md` and `verification.md`.
|
||||
Most wrong answers here come from assuming the CSS is already coherent.
|
||||
3. **Templates over invention.** `templates/components/` and `templates/pages/`
|
||||
exist so ten parallel agents produce one house style, not ten.
|
||||
4. **The gate is `npm run verify` plus the relevant checklist.** Green tests
|
||||
with deleted assertions is a failed task.
|
||||
5. **Report what you did not do.** Partial work with an honest boundary is
|
||||
useful; silent narrowing is not.
|
||||
|
||||
## Task flow
|
||||
|
||||
```
|
||||
plans/astro-refactor/task-NN.md ← the brief
|
||||
↓
|
||||
git worktree add ../af-task-NN ← isolation
|
||||
↓
|
||||
agent loads .agents/agents/<role>.md + its skills
|
||||
↓
|
||||
checklists/before-*.md ← self-gate
|
||||
↓
|
||||
npm run verify ← hard gate
|
||||
↓
|
||||
reviewer agent on the diff ← merge gate
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: astro-architect
|
||||
description: Owns the Astro scaffold — config, routing, layouts, build pipeline, and the publishing decision. Use for task 01 and any later change to astro.config.mjs, package.json, or the deploy path. Do not use for component or page work.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own the foundation. Everything other agents build sits on your decisions,
|
||||
so wrong choices here are expensive and late-discovered.
|
||||
|
||||
**Read first**: `.agents/context/architecture.md`, `.agents/context/publishing.md`,
|
||||
`.agents/rules/astro.md`. **Load skill**: `astro-page`.
|
||||
|
||||
## You own
|
||||
|
||||
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/`, the CI
|
||||
workflow, and the `pages`-branch publishing decision. You are the **only**
|
||||
writer of these. Other agents report problems with them; they do not edit.
|
||||
|
||||
## Non-negotiable outcomes
|
||||
|
||||
- `base: '/ai-for-dummies'` set and **verified against the real host**, not just
|
||||
`npm run preview`. Base-path bugs are the most likely production-only failure.
|
||||
- Every existing URL resolves identically, trailing slash included.
|
||||
- Zero JS by default. Astro ships none unless a component asks.
|
||||
- `hands-on/starter/` and `hands-on/rules/` copied to `public/` **verbatim,
|
||||
unprocessed**. They are lab fixtures; the exercise is that they are plain files.
|
||||
- No UI framework, no CSS framework, no runtime dependencies.
|
||||
|
||||
## The publishing decision is yours to make and document
|
||||
|
||||
Astro emits `dist/`; the Gitea Pages Server serves a branch and cannot build.
|
||||
Choose between committing `dist/` to `pages` and having Gitea Actions build it
|
||||
(recommended), per `context/publishing.md`. Then **rewrite
|
||||
`docs/operations-guide.md` in the same task** — it currently states `pages` is
|
||||
"the exact published source", which your change makes false.
|
||||
|
||||
Known trap: this Gitea's act-runner registration lives in an `emptyDir`, so a
|
||||
pod restart silently kills CI. Put that in the runbook.
|
||||
|
||||
## Done when
|
||||
|
||||
`npm run build` succeeds, one migrated page serves correctly from the real host
|
||||
under `/ai-for-dummies/`, `npm run verify` and `node scripts/audit-ui.mjs` are
|
||||
green, and the operations guide matches reality.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: component-builder
|
||||
description: Builds one Astro component per task from the project templates. Use for the component-extraction tasks (05-09). Do not use for page migration, token changes, or verify.mjs.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You build **one component per task**, from a template, in your own worktree.
|
||||
Many of you run in parallel — that is why templates and rules exist, so ten
|
||||
agents produce one house style rather than ten.
|
||||
|
||||
**Read first**: `.agents/rules/componentization.md`, `.agents/rules/astro.md`,
|
||||
`.agents/rules/theming.md`. **Load skills**: `astro-component`, `design-tokens`.
|
||||
|
||||
## Scope discipline
|
||||
|
||||
Your task names one component. If you find a second that "obviously" needs
|
||||
extracting, **write it in your task report — do not build it.** Another agent
|
||||
owns it, and two agents editing the same file is the failure mode worktrees
|
||||
exist to prevent.
|
||||
|
||||
You may not edit `tokens.css`, `verify.mjs`, `astro.config.mjs`, or
|
||||
`src/content/config.ts`. If you need a change there, report it.
|
||||
|
||||
## Rules that bite
|
||||
|
||||
- No raw hex, no px font sizes, no ad-hoc breakpoints. Tokens only.
|
||||
- No `client:*` unless genuinely interactive, with written justification.
|
||||
- Every ARIA attribute from the markup you replace survives. `verify.mjs`
|
||||
asserts several by name.
|
||||
- Keep the `gap:1px` over a coloured parent trick where the original used it.
|
||||
- Under ~120 lines of markup. More means two components.
|
||||
|
||||
## Done when
|
||||
|
||||
`.agents/checklists/before-component.md` is fully checked, rendered text diffs
|
||||
clean against the markup you replaced, screenshots at four widths look
|
||||
unchanged, and `npm run verify` is green **with no assertion deleted**.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: content-i18n-migrator
|
||||
description: Moves bilingual copy out of app.js and catalog.js into typed Astro content collections without altering a string. Use for tasks 03-04 and any later content relocation. Do not use for markup or styling.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own `src/content/` and `src/content/config.ts`, and you are their only
|
||||
writer. Your job is a lossless move, not an edit.
|
||||
|
||||
**Read first**: `.agents/rules/content-i18n.md`. **Load skill**: `content-migration`.
|
||||
|
||||
## What you are moving
|
||||
|
||||
~50 `{ en, pt }` keys from `app.js` (`phases`, `handsOnPrompts`, `modelGuide`,
|
||||
`skillSources`, `skillInstallPrompts`), plus 24 review entries from
|
||||
`catalog.js` and `submitted-catalog.js`.
|
||||
|
||||
These are hand-written translations with deliberate tone. **Copy them
|
||||
mechanically. Never retype.** Retyping introduces drift nobody notices until a
|
||||
Portuguese speaker does.
|
||||
|
||||
## Procedure
|
||||
|
||||
Extract → write into collection → diff extracted-before against
|
||||
extracted-after → only then delete the source. If the diff is not empty, you
|
||||
changed content. Fix it before continuing.
|
||||
|
||||
Both locales required in the schema. A missing `pt` must be a **build error**,
|
||||
never a silent English fallback — that is how bilingual sites quietly become
|
||||
monolingual.
|
||||
|
||||
## The trap that will catch you
|
||||
|
||||
`skill-reviews/improved/**/SKILL.md` is generated from `catalog.js` by
|
||||
`scripts/build-skill-review.mjs` and the output is **committed**. Move
|
||||
`catalog.js` and the generator keeps running against nothing — silently. Either
|
||||
re-point it or replace it, and update `package.json`, `README.md`,
|
||||
`docs/operations-guide.md`, and the review desk footer, all of which reference it.
|
||||
|
||||
Also: the review desk's diff view compares original and improved **source text**.
|
||||
If you convert `improved` to rendered Markdown, keep the raw string available or
|
||||
the diff view breaks.
|
||||
|
||||
## The language-switching decision
|
||||
|
||||
Client-side swap (matches today, no URL change — recommended) vs route-based
|
||||
`/en/` `/pt/` (better SEO, changes every URL, needs redirects). Surface it, get
|
||||
a decision, record it. Either way `<html lang>` tracks the active language.
|
||||
|
||||
## Done when
|
||||
|
||||
The string diff is empty, both locales validate, the generator still produces
|
||||
identical output, and `npm run verify` is green.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: design-system-keeper
|
||||
description: Owns src/styles/tokens.css — the palette, type scale, and breakpoints. Use for task 02 and any later token change or check-tokens failure. Do not use for building components.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own the token layer and are its only writer. Your job is to make "maintain
|
||||
the same styles" true and verifiable.
|
||||
|
||||
**Read first**: `.agents/context/design-system.md` — it documents three drifting
|
||||
palettes and a broken `@font-face`. **Load skills**: `design-tokens`,
|
||||
`visual-regression`.
|
||||
|
||||
## The two decisions you must surface, not silently make
|
||||
|
||||
1. **Three palettes → one.** `--ink` exists as `#172f42`, `#122534`, `#173044`;
|
||||
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||
sub-perceptual and can be canonicalized. `--blue` (`#527f9f` vs `#215675`) is
|
||||
visibly different — screenshot both and get a human decision.
|
||||
2. **The fonts have never rendered.** The `@font-face` in `styles.css:1` points
|
||||
`src:` at a Google Fonts *stylesheet*, so Manrope and DM Mono have always
|
||||
fallen back to Arial and generic monospace. Self-hosting them is a redesign,
|
||||
not a refactor. Default: delete the dead rule, declare the stacks that
|
||||
actually render. Escalate if someone wants the real fonts.
|
||||
|
||||
## You own
|
||||
|
||||
`src/styles/tokens.css`, `src/styles/base.css`, and
|
||||
`.agents/scripts/check-tokens.mjs`.
|
||||
|
||||
Deliver: one value per token, a named type scale (`--step-*`) replacing 14 ad-hoc
|
||||
`clamp()` triples, five named breakpoints replacing sixteen, and an enforcement
|
||||
script wired into `npm run verify`.
|
||||
|
||||
## Preserve the house style
|
||||
|
||||
Flat colour blocks, `1px` hairlines, near-zero radius, tight negative tracking
|
||||
on display type, `Georgia, serif` emphasis spans, and the `gap:1px` over a
|
||||
coloured parent trick used for grid separators. That last one is deliberate —
|
||||
never convert it to `border`.
|
||||
|
||||
## Done when
|
||||
|
||||
Every token has exactly one value, `check-tokens.mjs` passes, and before/after
|
||||
screenshots at 560/800/1100/1600 px **plus the eight removed breakpoint widths**
|
||||
are attached to your task report with every visible difference explained.
|
||||
@@ -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,42 @@
|
||||
---
|
||||
name: page-migrator
|
||||
description: Migrates one hand-written HTML page to an Astro route with identical URL, content, and JS budget. Use for the page-migration tasks (10-16). Do not use for component extraction or config changes.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You migrate **one page per task**. The bar is that a visitor cannot tell.
|
||||
|
||||
**Read first**: `.agents/context/architecture.md`, `.agents/context/verification.md`.
|
||||
**Load skills**: `astro-page`, `content-migration`, `visual-regression`.
|
||||
|
||||
## Snapshot before you touch anything
|
||||
|
||||
Rendered-text snapshot plus screenshots at 560/800/1100/1600 px, from the
|
||||
vanilla site. Without a before, you cannot prove an after. This is not optional
|
||||
and it is the step agents skip.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **URL identical**, trailing slash included. Internal links go through
|
||||
`import.meta.env.BASE_URL`, never a hand-written absolute path.
|
||||
- **Query params survive** — the review desk uses `?author=`, `?skill=`,
|
||||
`?view=`, `?file=`, `?compare=`, `?render=` and they are shared externally.
|
||||
- **JS budget does not grow.** A page that shipped zero JS still ships zero.
|
||||
Seven of ten pages are in that category.
|
||||
- Every `data-*` hook, ARIA attribute, `<title>`, and `<meta>` preserved.
|
||||
- Reuse existing components before creating new ones.
|
||||
- Do **not** port `responsive.css` wholesale. Take what the page needs; prove
|
||||
the rest is dead and delete it.
|
||||
|
||||
## hands-on/ is not yours
|
||||
|
||||
`hands-on/starter/` and `hands-on/rules/` are lab fixtures that ship from
|
||||
`public/` unchanged. Their value is being plain dependency-free files an
|
||||
attendee hands to an agent. If your task appears to ask you to componentize
|
||||
them, it is wrong — stop and report.
|
||||
|
||||
## Done when
|
||||
|
||||
`.agents/checklists/before-page.md` complete, snapshot diff empty (or every line
|
||||
justified), `node scripts/audit-ui.mjs` and `npm run verify` green, screenshots
|
||||
compared, and your task report lists what you deliberately left alone.
|
||||
@@ -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,50 @@
|
||||
---
|
||||
name: verification-engineer
|
||||
description: Keeps scripts/verify.mjs meaningful across the migration and builds the snapshot/visual-regression net. Use for tasks 18-19 and whenever a verify assertion needs re-pointing. The only role permitted to reduce coverage.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You own `scripts/verify.mjs`, `scripts/audit-ui.mjs`, `.agents/snapshots/`, and
|
||||
the visual-regression tooling. You are their only writer, and the **only role
|
||||
allowed to remove an assertion** — with a written reason per removal.
|
||||
|
||||
**Read first**: `.agents/context/verification.md`. **Load skills**:
|
||||
`verify-contract`, `visual-regression`.
|
||||
|
||||
## Why the role exists
|
||||
|
||||
42 assertions pin this site's real content. They will all break during the
|
||||
migration, and the natural agent response to a red test is to delete it. That
|
||||
turns a content-loss bug into a green build. You are the check on that.
|
||||
|
||||
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease across the
|
||||
migration.
|
||||
|
||||
## Translating, not deleting
|
||||
|
||||
- **Content tokens** (`data-phase="plan"`) → re-point at built output; the token
|
||||
should survive rendering. If it does not, a component dropped content.
|
||||
- **Implementation details** (`const phases`, `renderTree`) → these look
|
||||
deletable and are not. They pin a feature. Replace with an output-level
|
||||
assertion of the same feature.
|
||||
- **Asset versions** (`app.js?v=…`) → assert the built HTML references a hashed
|
||||
asset.
|
||||
|
||||
## Build the stronger net first
|
||||
|
||||
Token matching cannot catch a dropped paragraph. Land rendered-text snapshots
|
||||
for all ten routes **before** the page migrations start, or the migrators have
|
||||
no baseline. This is early, blocking work.
|
||||
|
||||
## Fix the audit gap
|
||||
|
||||
`audit-ui.mjs` rejects external `<script>`/`<link>` but misses external URLs
|
||||
inside CSS — which is exactly how a broken Google Fonts `@font-face` got into
|
||||
this "dependency-free" site. Add `@import`, `src: url(https:…)`, and
|
||||
`url(https:…)` detection.
|
||||
|
||||
## Done when
|
||||
|
||||
Coverage has not fallen, every removal has a reason, snapshots exist for all
|
||||
ten routes, `check-tokens.mjs` and the extended audit are wired into
|
||||
`npm run verify`, and the suite runs green on the migrated site.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Checklist: before you call a component done
|
||||
|
||||
- [ ] It appears (or will appear) in **three** places, or has a name a person says out loud
|
||||
- [ ] Lives in the right folder: `primitives/`, `blocks/`, or `islands/`
|
||||
- [ ] Typed `interface Props`; every field intentional; no `any`
|
||||
- [ ] **No raw hex, px font sizes, or ad-hoc breakpoints** — tokens only
|
||||
- [ ] Ships zero JS, or has a `client:*` directive with a written justification
|
||||
- [ ] Markup under ~120 lines
|
||||
- [ ] Native elements: `<button>` for actions, `<a>` for navigation
|
||||
- [ ] Keyboard: tab to it, operate with Enter/Space, focus ring visible
|
||||
- [ ] Interactive state exposed via ARIA (`aria-pressed`, `aria-current`), not just colour
|
||||
- [ ] Any motion respects `prefers-reduced-motion` and animates only `transform`/`opacity`
|
||||
- [ ] Renders correctly at 560 / 800 / 1100 / 1600 px
|
||||
- [ ] Both `en` and `pt` strings present; no hard-coded copy
|
||||
- [ ] `npm run verify` green with no assertion deleted
|
||||
@@ -0,0 +1,12 @@
|
||||
# Checklist: before you merge a task branch
|
||||
|
||||
- [ ] Rebased on current `origin/main`, conflicts resolved in the worktree
|
||||
- [ ] `npm run verify` and `node scripts/audit-ui.mjs` both green
|
||||
- [ ] **Assertion count in `verify.mjs` did not fall** (`grep -c 'throw new Error' scripts/verify.mjs`)
|
||||
- [ ] Only files in your task's scope changed — `git diff --stat origin/main` matches the brief
|
||||
- [ ] No contended file edited unless you own it (see `rules/git-worktrees.md`)
|
||||
- [ ] No `.serena/`, `__pycache__/`, `dist/`, or scratch files staged
|
||||
- [ ] Commit message: what, why, and what you deliberately did not do
|
||||
- [ ] Task report written: verified behaviours, known gaps, follow-ups
|
||||
- [ ] `reviewer` agent has read the diff
|
||||
- [ ] Worktree and branch cleaned up after merge
|
||||
@@ -0,0 +1,14 @@
|
||||
# Checklist: before you call a page migrated
|
||||
|
||||
- [ ] URL identical to the old one, **trailing slash included**
|
||||
- [ ] Any query params the old page honoured still work (`?author=`, `?skill=`, `?view=`, `?file=`, `?compare=`, `?render=`)
|
||||
- [ ] Rendered text content diffs clean against the pre-migration snapshot — or every difference is listed and justified
|
||||
- [ ] Same number of JS bytes or fewer; a previously-zero-JS page still ships zero
|
||||
- [ ] `<title>`, `<meta name="description">`, and `<meta name="viewport">` preserved
|
||||
- [ ] One `<h1>`; heading levels do not skip
|
||||
- [ ] Every ARIA attribute from the original survived (`verify.mjs` asserts several)
|
||||
- [ ] No external `<script>`, `<link>`, `@import`, or `url()` — `node scripts/audit-ui.mjs` green
|
||||
- [ ] Internal links go through `BASE_URL`, not hand-written absolute paths
|
||||
- [ ] Screenshots at 560 / 800 / 1100 / 1600 px compared against the old page
|
||||
- [ ] `<html lang>` correct and still switching with the language toggle
|
||||
- [ ] `npm run verify` green with no assertion deleted
|
||||
@@ -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,107 @@
|
||||
# Context: the design system (as it actually is)
|
||||
|
||||
Read this before touching CSS. Everything here was extracted from the current
|
||||
files, not assumed.
|
||||
|
||||
## There is no single palette. There are three.
|
||||
|
||||
The same semantic names carry different values depending on which stylesheet
|
||||
loaded them:
|
||||
|
||||
| Token | `styles.css`, `rules/styles.css` | `chapters.css`, `skills-review/styles.css` | `hands-on/*/styles.css` |
|
||||
| --- | --- | --- | --- |
|
||||
| `--paper` | `#f5f4f1` | `#f6f3ed` | `#f4f3ef` |
|
||||
| `--ink` | `#172f42` | `#122534` | `#173044` |
|
||||
| `--muted` | `#697b89` | `#65717a` | `#687d8c` |
|
||||
| `--line` | `#d8dee2` | `#d0d5d2` | `#d5dde1` |
|
||||
| `--blue` | `#527f9f` | `#215675` | `#5683a1` |
|
||||
| `--gold` | `#efc76b` | `#ebbf58` | `#efc86d` |
|
||||
| `--accent` | `#7c78a8` | — | — |
|
||||
| `--deep` | `#102536` | — | — |
|
||||
| `--red` | — | `#a7483f` (chapters only) | — |
|
||||
| `--violet` | — | `#6b668f` (review desk only) | — |
|
||||
|
||||
Most deltas are a few units per channel — drift, not intent. `--blue` is the
|
||||
exception: `#527f9f` vs `#215675` is a visible difference and may be deliberate.
|
||||
|
||||
**Decision required before any component work** (task 02). Options:
|
||||
|
||||
- **Canonicalize** to one palette. Recommended. The sub-perceptual deltas
|
||||
collapse; only `--blue` needs a human's eye on a before/after screenshot.
|
||||
- **Keep three named surfaces** (`--surface-guide`, `--surface-chapter`,
|
||||
`--surface-lab`) if the drift turns out to be intentional per section.
|
||||
|
||||
Do not "just pick one" silently in the middle of another task. This is its own
|
||||
reviewed change with visual diffs attached.
|
||||
|
||||
## The typography you see is not the typography that was written
|
||||
|
||||
`styles.css` line 1:
|
||||
|
||||
```css
|
||||
@font-face{font-family:Manrope;src:url('https://fonts.googleapis.com/css2?family=DM+Mono&family=Manrope:wght@400;600;700;800&display=swap')}
|
||||
```
|
||||
|
||||
`src:` points at a **CSS stylesheet**, not a font file. No browser can load a
|
||||
font from that, so:
|
||||
|
||||
- every `font-family:Manrope,Arial,sans-serif` renders as **Arial**
|
||||
- every `font:… 'DM Mono',monospace` renders as the **generic monospace** face
|
||||
- there are no `@font-face` blocks anywhere else and zero font files in the repo
|
||||
- `scripts/audit-ui.mjs` only rejects external `<link>`/`<script>` tags, so this
|
||||
slipped through the "dependency-free" audit
|
||||
|
||||
**This is a trap for the refactor.** Self-hosting Manrope and DM Mono in Astro
|
||||
is the obvious "fix" — and it would change how every page looks, violating
|
||||
"maintain the same styles". Treat it as an explicit product decision:
|
||||
|
||||
- **Keep current rendering**: delete the dead `@font-face`, replace the font
|
||||
stacks with what actually renders today (`Arial, sans-serif` /
|
||||
`ui-monospace, monospace`). Zero visual change. Honest CSS.
|
||||
- **Adopt the intended fonts**: self-host the woff2 files in `public/fonts/`,
|
||||
add real `@font-face` with `font-display:swap`. Better-looking, but it is a
|
||||
redesign and needs sign-off plus fresh screenshots.
|
||||
|
||||
Default to the first unless a human says otherwise.
|
||||
|
||||
## Type scale
|
||||
|
||||
`Georgia, serif` is used deliberately for emphasis (`h1 em`, `.hero em`) and is
|
||||
real — it is a system font, so it does render. Keep it.
|
||||
|
||||
Sizes are all `clamp()`, roughly:
|
||||
|
||||
| Role | Value |
|
||||
| --- | --- |
|
||||
| Display / `h1` | `clamp(56px,9vw,126px)` |
|
||||
| Section `h2` | `clamp(36px,5vw,65px)` |
|
||||
| Sub-head | `clamp(24px,3vw,38px)` |
|
||||
| Pull-quote | `clamp(22px,3vw,36px)` |
|
||||
| Body | `15px/1.6` … `18px` |
|
||||
| Eyebrow / label | `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,54 @@
|
||||
# Context: publishing, and what a build step changes
|
||||
|
||||
## How it works today
|
||||
|
||||
The Gitea Pages Server serves the **`pages` branch tree directly**. There is no
|
||||
build. `main` and `pages` end up with byte-identical trees; `pages` exists only
|
||||
because the Pages Server publishes a branch, not a directory.
|
||||
|
||||
Live at `https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/`.
|
||||
|
||||
Full procedure, including the fact that `merge --ff-only main` fails (the
|
||||
histories diverged), is in [`../../docs/operations-guide.md`](../../docs/operations-guide.md).
|
||||
|
||||
## What Astro changes
|
||||
|
||||
Astro emits `dist/`. The Pages Server cannot run a build, so **something has to
|
||||
put built output on `pages`**. Pick one, deliberately, in task 01:
|
||||
|
||||
| Option | How | Cost |
|
||||
| --- | --- | --- |
|
||||
| **A. Build locally, commit `dist/` to `pages`** | `npm run build`, copy `dist/*` into the `pages` worktree, commit | `pages` stops being "the exact source". Diffs become unreadable. Publishing depends on one workstation. Simple, no new infra. |
|
||||
| **B. Gitea Actions builds and pushes `pages`** | workflow on `main` → `npm ci && npm run build` → force-push `dist/` to `pages` | `pages` becomes a machine-owned branch (force-push is fine *because* nothing else writes it). Needs the act-runner to be healthy. **Recommended.** |
|
||||
| **C. Serve from a container instead** | drop Pages Server for an nginx pod behind the existing ingress | most control, most infra, changes the URL story |
|
||||
|
||||
**Recommended: B**, with A as the documented manual fallback for when the runner
|
||||
is down. Note the known failure mode: this Gitea's act-runner registration lives
|
||||
in an `emptyDir`, so a pod restart silently kills CI until re-registered. The
|
||||
runbook must say "if the site stopped updating, check the runner first".
|
||||
|
||||
Whichever you pick, `docs/operations-guide.md` must be rewritten in the same
|
||||
task — it currently promises `pages` is "the exact published source", and that
|
||||
stops being true under A and B.
|
||||
|
||||
## Base path
|
||||
|
||||
The site is served from a **subdirectory**: `/ai-for-dummies/`. Astro needs
|
||||
`base: '/ai-for-dummies'` in `astro.config.mjs`, and every internal link must go
|
||||
through `import.meta.env.BASE_URL` or Astro's `<a href={...}>` helpers rather
|
||||
than a hand-written absolute `/models/`.
|
||||
|
||||
This is the single most likely source of "works locally, 404s in production" in
|
||||
this migration. Verify it on the real host, not just `npm run preview`.
|
||||
|
||||
## Verification before you call it published
|
||||
|
||||
```bash
|
||||
curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/?v=$(git rev-parse --short HEAD)"
|
||||
```
|
||||
|
||||
The `?v=` cache-buster matters: the Pages Server caches, and a stale 200 looks
|
||||
exactly like a successful deploy. Check one nested route
|
||||
(`/ai-for-dummies/skills-review/`) and one static asset too — the base-path bug
|
||||
shows up on assets first.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Context: the verification contract
|
||||
|
||||
`scripts/verify.mjs` is 13 KB, 42 `throw new Error` sites, 16 checkpoints. It
|
||||
reads 26 source files and asserts that specific **string tokens** appear in
|
||||
them — `data-phase="plan"`, `renderTree`, `.change-lens`,
|
||||
`styles.css?v=20260904-vote-widget`, and so on.
|
||||
|
||||
## Why this matters more than it looks
|
||||
|
||||
These assertions are the only thing standing between this site and silent
|
||||
content loss during a large refactor. They are also **all going to break**,
|
||||
because they assert against files that will stop existing.
|
||||
|
||||
The failure mode to guard against: an agent runs `npm run verify`, sees red,
|
||||
and "fixes" it by deleting the assertion. The suite goes green and the site
|
||||
loses a section. **Deleting an assertion is a change that requires review, the
|
||||
same as deleting a feature.**
|
||||
|
||||
## How the contract must evolve
|
||||
|
||||
Three kinds of assertion, three different fates:
|
||||
|
||||
| Kind | Example | Fate |
|
||||
| --- | --- | --- |
|
||||
| **Content presence** | `'data-phase="plan"'` in `full-guide/index.html` | Re-point at built output (`dist/`) — the token should survive rendering. If it does not, the component dropped content. |
|
||||
| **Implementation detail** | `'const phases'`, `'renderTree'` in `app.js` | Obsolete. Replace with an assertion about *behaviour or output*, never delete outright. |
|
||||
| **Cache-busting version** | `'app.js?v=20260904-vote-widget'` | Obsolete — Astro hashes assets. Replace with "the built HTML references a hashed asset". |
|
||||
|
||||
**Rule: the assertion count must not fall.** Every removed token is replaced by
|
||||
one that pins the same user-visible fact against the new architecture. The
|
||||
verification engineer owns this and is the only role allowed to reduce coverage,
|
||||
with a written reason per removal.
|
||||
|
||||
## The stronger check to add
|
||||
|
||||
Token-matching is brittle. During the migration, add a **rendered-output diff**:
|
||||
snapshot the current site's DOM text content per route, then assert the Astro
|
||||
build produces the same text. That catches dropped paragraphs the way token
|
||||
matching cannot.
|
||||
|
||||
```bash
|
||||
# before migrating a page, from the vanilla site:
|
||||
node .agents/scripts/snapshot-route.mjs /models/ > .agents/snapshots/models.txt
|
||||
# after: same script against dist/, diff must be empty (or reviewed)
|
||||
```
|
||||
|
||||
See [`../skills/verify-contract/SKILL.md`](../skills/verify-contract/SKILL.md).
|
||||
|
||||
## Also in the suite
|
||||
|
||||
`scripts/audit-ui.mjs` asserts every page has a viewport meta and **no external
|
||||
`<script>`/`<link>`**. Keep it and extend it: it currently misses external URLs
|
||||
inside CSS (`@font-face src`, `@import`, `url()`), which is exactly how the
|
||||
broken Google Fonts request in `styles.css:1` got in.
|
||||
@@ -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,76 @@
|
||||
# Rule: quality gates
|
||||
|
||||
Three tiers. Each is scoped so that **many agents committing in parallel
|
||||
worktrees stay fast** — the whole point is that a gate you are tempted to skip
|
||||
is not a gate.
|
||||
|
||||
| Tier | Hook | Scope | Budget | Runs |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `pre-commit` | **staged files only** (lint-staged) | < 3s | every commit |
|
||||
| 2 | `pre-push` | whole project: build + verify + audit | < 90s | every push |
|
||||
| 3 | CI (Gitea Actions) | tier 2 + visual regression | minutes | every push to `main` |
|
||||
|
||||
Tier 1 must stay under a few seconds. If it creeps, move the check to tier 2.
|
||||
An agent that waits 40s per commit will start passing `--no-verify`, and then
|
||||
you have no gate at all.
|
||||
|
||||
## Tier 1 — pre-commit (lint-staged)
|
||||
|
||||
Formats and lints **only what you staged**. Scoped by construction, so ten
|
||||
parallel worktrees do ten small jobs, not ten full-project sweeps.
|
||||
|
||||
- Prettier + ESLint on `*.{js,mjs,ts,astro}`
|
||||
- Prettier + Stylelint on `*.css`
|
||||
- `check-tokens.mjs` on changed `.astro`/`.css` — catches raw hex before it lands
|
||||
- Prettier on `*.{json,md}`
|
||||
|
||||
## Tier 2 — pre-push
|
||||
|
||||
The real gate:
|
||||
|
||||
```
|
||||
astro check types
|
||||
astro build it compiles
|
||||
verify.mjs 42 content assertions — count must not fall
|
||||
audit-ui.mjs no external runtime dependencies
|
||||
check-tokens.mjs full sweep
|
||||
```
|
||||
|
||||
## Tier 3 — CI
|
||||
|
||||
Tier 2 plus screenshot comparison against `.agents/snapshots/`. Only CI has the
|
||||
budget for it.
|
||||
|
||||
## Bypassing
|
||||
|
||||
`--no-verify` is allowed exactly once: a work-in-progress commit **on your own
|
||||
task branch that you will rebase away**. It is never allowed on a commit you
|
||||
intend to merge, and the pre-push gate has no bypass.
|
||||
|
||||
If a gate is wrong, fix the gate in its own commit. Do not route around it.
|
||||
|
||||
## Parallelism
|
||||
|
||||
- Hooks are **per-worktree**. Git's `index.lock` is per-worktree, so parallel
|
||||
commits do not contend.
|
||||
- The heavy tier-2 gate takes a **lock** (`.git/af-gate.lock`, shared across
|
||||
worktrees) so ten agents pushing at once do not run ten concurrent builds and
|
||||
thrash the machine. Waiters queue; they do not fail.
|
||||
- `npm ci` in a fresh worktree should use `--prefer-offline` to avoid registry
|
||||
contention when several spin up at once.
|
||||
|
||||
## The silent-failure mode you must know about
|
||||
|
||||
Husky sets `core.hooksPath` to `.husky/_`, and **`.husky/_` is generated by
|
||||
`npm install`, not committed**. A fresh `git worktree add` therefore has hooks
|
||||
configured but the directory missing — so **hooks silently do not run**. Every
|
||||
commit passes. Nothing is checked.
|
||||
|
||||
`.agents/scripts/worktree.sh start` runs the install and then verifies. Check
|
||||
manually any time you did not use it:
|
||||
|
||||
```bash
|
||||
.agents/scripts/verify-hooks.sh
|
||||
```
|
||||
|
||||
Run this first in any worktree you did not create with the script.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Rule: git worktrees
|
||||
|
||||
This project teaches worktrees. It should use them properly.
|
||||
|
||||
## One task, one worktree, one agent
|
||||
|
||||
```bash
|
||||
# from the main checkout
|
||||
git worktree add ../af-task-07 -b refactor/task-07-route-cards
|
||||
cd ../af-task-07
|
||||
npm ci
|
||||
```
|
||||
|
||||
Naming: directory `../af-task-NN`, branch `refactor/task-NN-<slug>`. Both
|
||||
derived from the task file so the mapping is never ambiguous.
|
||||
|
||||
## Why isolation matters here
|
||||
|
||||
The migration runs many agents in parallel over the same small set of files
|
||||
(`tokens.css`, `verify.mjs`, `astro.config.mjs` are contended). Worktrees give
|
||||
each agent its own working directory over one object store — cheap, and no
|
||||
agent can see another's half-finished state.
|
||||
|
||||
The failure mode without them: two agents both "fix" `verify.mjs`, and the
|
||||
second overwrites the first's assertions.
|
||||
|
||||
## Contended files
|
||||
|
||||
These are touched by many tasks. Whoever owns them per the plan is the **only**
|
||||
writer; everyone else opens an issue in their task report instead of editing:
|
||||
|
||||
| File | Owner |
|
||||
| --- | --- |
|
||||
| `src/styles/tokens.css` | `design-system-keeper` |
|
||||
| `scripts/verify.mjs` | `verification-engineer` |
|
||||
| `astro.config.mjs`, `package.json` | `astro-architect` |
|
||||
| `src/content/config.ts` | `content-i18n-migrator` |
|
||||
|
||||
## Before you start
|
||||
|
||||
1. `git fetch origin && git rebase origin/main` — start from current `main`.
|
||||
2. Read your task file end to end before writing anything.
|
||||
3. Confirm your task's dependencies are merged. Task files list them.
|
||||
|
||||
## Before you finish
|
||||
|
||||
1. `npm run verify` green — without deleting assertions.
|
||||
2. The relevant checklist in [`../checklists/`](../checklists/) complete.
|
||||
3. `git rebase origin/main` again, resolve conflicts in your worktree.
|
||||
4. Task report: what changed, what you verified, **what you did not do**.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
cd - # back to the main checkout
|
||||
git worktree remove ../af-task-07
|
||||
git branch -d refactor/task-07-route-cards
|
||||
```
|
||||
|
||||
Stale worktrees hold locks and confuse the next agent. `git worktree list`
|
||||
should be short.
|
||||
|
||||
## Never
|
||||
|
||||
- Never work directly on `main`.
|
||||
- Never force-push a shared branch. The `pages` branch is the sole exception,
|
||||
and only if CI owns it (see [`../context/publishing.md`](../context/publishing.md)).
|
||||
- Never `git add -A` from the repository root. This repo has untracked local
|
||||
scratch (`.serena/`, `scripts/inspect.py`) that must not be swept into a commit.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Rule: theming
|
||||
|
||||
Binding for every colour, font size, spacing value, and breakpoint.
|
||||
|
||||
**Read [`../context/design-system.md`](../context/design-system.md) first.** The
|
||||
current CSS has three drifting palettes and a broken `@font-face`. This rule
|
||||
describes the target; that file describes what you are migrating from.
|
||||
|
||||
## One token layer
|
||||
|
||||
Every value comes from `src/styles/tokens.css`. If a component needs a value
|
||||
that is not a token, either it is a genuine one-off (justify it in a comment) or
|
||||
the token layer is missing something (add it there, not inline).
|
||||
|
||||
```css
|
||||
/* forbidden */
|
||||
color: #172f42;
|
||||
color: rgba(23,47,66,.6);
|
||||
|
||||
/* required */
|
||||
color: var(--ink);
|
||||
color: var(--muted);
|
||||
```
|
||||
|
||||
No raw hex outside `tokens.css`. `.agents/scripts/check-tokens.mjs` enforces it;
|
||||
wire it into `npm run verify`.
|
||||
|
||||
## Semantic names, not literal ones
|
||||
|
||||
`--ink`, `--paper`, `--muted`, `--line`, `--accent`, `--gold`, `--blue`,
|
||||
`--deep` are the existing vocabulary. Keep it — it is already semantic and the
|
||||
team reads it fluently. Do not rename to `--color-neutral-900`.
|
||||
|
||||
If a genuine second surface is needed, extend semantically
|
||||
(`--surface-lab`, `--ink-inverse`), never numerically.
|
||||
|
||||
## Type scale
|
||||
|
||||
Replace the 14 ad-hoc `clamp()` triples with named steps:
|
||||
|
||||
```css
|
||||
--step-display: clamp(56px, 9vw, 126px); /* h1 */
|
||||
--step-6: clamp(36px, 5vw, 65px); /* section h2 */
|
||||
--step-5: clamp(24px, 3vw, 38px); /* sub-head */
|
||||
--step-4: clamp(22px, 3vw, 36px); /* pull-quote */
|
||||
--step-1: 15px; /* body */
|
||||
--step-0: 11px; /* eyebrow / label */
|
||||
```
|
||||
|
||||
The eyebrow treatment (`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.
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
// Fails when a raw colour, px font-size, or ad-hoc breakpoint appears outside
|
||||
// the token layer. A rule nobody checks is a suggestion — wire this into
|
||||
// `npm run verify`.
|
||||
//
|
||||
// Usage: node .agents/scripts/check-tokens.mjs [srcDir]
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join, extname } from 'node:path';
|
||||
|
||||
// lint-staged appends staged file paths; a bare run sweeps `src`.
|
||||
const ARGS = process.argv.slice(2);
|
||||
const TOKEN_FILES = ['tokens.css', 'base.css'];
|
||||
const ALLOWED_BREAKPOINTS = ['560px', '800px', '1100px', '1600px', '2200px'];
|
||||
|
||||
const walk = (dir) =>
|
||||
readdirSync(dir).flatMap((name) => {
|
||||
const path = join(dir, name);
|
||||
return statSync(path).isDirectory() ? walk(path) : [path];
|
||||
});
|
||||
|
||||
const targets = ARGS.length
|
||||
? ARGS.flatMap((arg) => (statSync(arg).isDirectory() ? walk(arg) : [arg]))
|
||||
: walk('src');
|
||||
|
||||
const findings = [];
|
||||
|
||||
for (const path of targets) {
|
||||
if (!['.astro', '.css'].includes(extname(path))) continue;
|
||||
if (TOKEN_FILES.some((allowed) => path.endsWith(allowed))) continue;
|
||||
|
||||
readFileSync(path, 'utf8').split('\n').forEach((line, index) => {
|
||||
const at = `${path}:${index + 1}`;
|
||||
|
||||
// Raw hex — the drifted-palette failure mode this whole layer exists to stop.
|
||||
const hex = line.match(/#[0-9a-fA-F]{3,8}\b/g);
|
||||
if (hex) findings.push(`${at}: raw hex ${hex.join(', ')} — use a token from tokens.css`);
|
||||
|
||||
// rgb()/hsl() literals are the same problem wearing a different hat.
|
||||
if (/\b(rgba?|hsla?)\(\s*\d/.test(line))
|
||||
findings.push(`${at}: raw colour function — use a token`);
|
||||
|
||||
// Hard-coded font sizes bypass the type scale.
|
||||
const fontSize = line.match(/font-size:\s*\d+(\.\d+)?px/);
|
||||
if (fontSize) findings.push(`${at}: hard-coded ${fontSize[0]} — use var(--step-*)`);
|
||||
|
||||
// Ad-hoc breakpoints are how sixteen of them accumulated last time.
|
||||
const media = line.match(/@media[^{]*?\(\s*(?:max|min)-width:\s*(\d+px)/);
|
||||
if (media && !ALLOWED_BREAKPOINTS.includes(media[1]))
|
||||
findings.push(`${at}: breakpoint ${media[1]} is not a named one (${ALLOWED_BREAKPOINTS.join(', ')})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (findings.length) {
|
||||
console.error(`token check failed — ${findings.length} finding(s):\n`);
|
||||
findings.forEach((finding) => console.error(` ${finding}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('token check passed');
|
||||
Executable
+45
@@ -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)`);
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tier 2 gate: types, build, content contracts, dependency audit, tokens.
|
||||
# Invoked by .husky/pre-push, and safe to run by hand at any time.
|
||||
#
|
||||
# Parallel-safe: takes a lock in the SHARED git dir, so ten agents pushing from
|
||||
# ten worktrees queue instead of running ten concurrent Astro builds and
|
||||
# thrashing the machine. Waiters block; they do not fail.
|
||||
set -euo pipefail
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
cd "$root"
|
||||
|
||||
# --git-common-dir resolves to the ONE shared .git across all worktrees, which
|
||||
# is exactly the scope we want the lock to cover.
|
||||
common=$(git rev-parse --git-common-dir)
|
||||
lock="$common/af-gate.lock"
|
||||
|
||||
exec 9>"$lock"
|
||||
if ! flock -n 9; then
|
||||
echo "gate: another worktree is running the gate — waiting for it…"
|
||||
flock 9
|
||||
fi
|
||||
|
||||
started=$(date +%s)
|
||||
step() { printf '\n\033[1m▸ %s\033[0m\n' "$1"; }
|
||||
|
||||
# Fail loudly rather than passing vacuously when the toolchain is not installed.
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "gate: node_modules missing — run 'npm ci --prefer-offline' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "types"
|
||||
npx --no-install astro check
|
||||
|
||||
step "build"
|
||||
npm run build
|
||||
|
||||
step "content contracts"
|
||||
npm run verify
|
||||
|
||||
# The assertion count is the thing agents are most tempted to "fix" downward.
|
||||
# Compare against origin/main and refuse a silent reduction.
|
||||
step "assertion coverage"
|
||||
current=$(grep -c 'throw new Error' scripts/verify.mjs)
|
||||
baseline=$(git show origin/main:scripts/verify.mjs 2>/dev/null | grep -c 'throw new Error' || echo "$current")
|
||||
if [ "$current" -lt "$baseline" ]; then
|
||||
echo "gate: verify.mjs coverage fell from $baseline to $current assertions." >&2
|
||||
echo " Only verification-engineer may reduce it, with a reason per removal." >&2
|
||||
echo " See .agents/context/verification.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " $current assertions (baseline $baseline)"
|
||||
|
||||
step "runtime dependency audit"
|
||||
node scripts/audit-ui.mjs
|
||||
|
||||
step "design tokens"
|
||||
node .agents/scripts/check-tokens.mjs
|
||||
|
||||
printf '\n\033[32mgate passed\033[0m in %ss\n' "$(( $(date +%s) - started ))"
|
||||
Executable
+29
@@ -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|/$||'
|
||||
Executable
+37
@@ -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);
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Guards the silent-failure mode described in .agents/rules/gates.md.
|
||||
#
|
||||
# Husky points core.hooksPath at `.husky/_`, but that directory is GENERATED by
|
||||
# `npm install` and is NOT committed. A fresh `git worktree add` therefore has
|
||||
# hooks configured and the directory missing — so every hook silently does
|
||||
# nothing and every commit passes unchecked.
|
||||
#
|
||||
# Run this in any worktree you did not create with worktree.sh.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
fail=0
|
||||
|
||||
path=$(git config --get core.hooksPath || true)
|
||||
if [ -z "$path" ]; then
|
||||
echo "✗ core.hooksPath is unset — husky was never installed here"
|
||||
fail=1
|
||||
else
|
||||
echo "✓ core.hooksPath = $path"
|
||||
fi
|
||||
|
||||
if [ ! -d "${path:-.husky/_}" ]; then
|
||||
echo "✗ ${path:-.husky/_}/ does not exist — HOOKS ARE NOT RUNNING"
|
||||
fail=1
|
||||
else
|
||||
echo "✓ ${path} exists"
|
||||
fi
|
||||
|
||||
for hook in pre-commit commit-msg pre-push; do
|
||||
if [ -f ".husky/$hook" ]; then
|
||||
echo "✓ .husky/$hook present"
|
||||
else
|
||||
echo "✗ .husky/$hook missing"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
echo "✗ node_modules missing — lint-staged and astro check cannot run"
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo
|
||||
echo "Fix: npm ci --prefer-offline (its prepare script regenerates .husky/_)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "hooks are live in this worktree"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Spin up (or tear down) an isolated worktree for one refactor task.
|
||||
#
|
||||
# .agents/scripts/worktree.sh start 07 route-cards
|
||||
# .agents/scripts/worktree.sh finish 07 route-cards
|
||||
#
|
||||
# One task, one worktree, one agent. See .agents/rules/git-worktrees.md
|
||||
set -euo pipefail
|
||||
|
||||
action=${1:?usage: worktree.sh <start|finish> <task-number> <slug>}
|
||||
number=${2:?task number, e.g. 07}
|
||||
slug=${3:?slug, e.g. route-cards}
|
||||
|
||||
dir="../af-task-${number}"
|
||||
branch="refactor/task-${number}-${slug}"
|
||||
plan="plans/astro-refactor/task-${number}-${slug}.md"
|
||||
|
||||
case "$action" in
|
||||
start)
|
||||
[ -f "$plan" ] || echo "warning: no plan at $plan — check the task number" >&2
|
||||
git fetch origin
|
||||
git worktree add "$dir" -b "$branch" origin/main
|
||||
|
||||
# Not optional. `.husky/_` is generated by install and is NOT committed, so
|
||||
# a fresh worktree has hooks configured but absent — every commit would pass
|
||||
# unchecked. --prefer-offline keeps ten parallel spin-ups off the registry.
|
||||
( cd "$dir" && npm ci --prefer-offline && .agents/scripts/verify-hooks.sh )
|
||||
|
||||
echo
|
||||
echo "worktree : $dir"
|
||||
echo "branch : $branch"
|
||||
echo "brief : $plan"
|
||||
echo
|
||||
echo "next: cd $dir && read the brief end to end before writing anything"
|
||||
;;
|
||||
finish)
|
||||
# Never remove a worktree with uncommitted work in it.
|
||||
if [ -n "$(git -C "$dir" status --porcelain)" ]; then
|
||||
echo "refusing: $dir has uncommitted changes" >&2
|
||||
git -C "$dir" status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
git worktree remove "$dir"
|
||||
echo "removed $dir — branch $branch kept; delete it after the merge lands"
|
||||
;;
|
||||
*)
|
||||
echo "unknown action: $action (expected start|finish)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: astro-component
|
||||
description: Build one Astro component for the ai-for-dummies site from the project templates. Use when creating a primitive, block, or island, or when splitting existing markup into a component.
|
||||
---
|
||||
|
||||
# Building an Astro component
|
||||
|
||||
## Decide it should exist
|
||||
|
||||
Three occurrences, or a name a person says out loud. Two is not enough — see
|
||||
[`../../rules/componentization.md`](../../rules/componentization.md).
|
||||
|
||||
Then place it:
|
||||
|
||||
- `primitives/` — no domain knowledge (Eyebrow, Rule, Callout, CodeBlock)
|
||||
- `blocks/` — composed, page-agnostic, takes props (RouteCard, PhasePanel)
|
||||
- `islands/` — interactive, justified, a leaf
|
||||
|
||||
## Start from a template
|
||||
|
||||
```bash
|
||||
cp .agents/templates/components/static-block.astro src/components/blocks/RouteCard.astro
|
||||
# interactive? use island.astro instead
|
||||
```
|
||||
|
||||
Templates exist so ten parallel agents produce one house style. Do not start
|
||||
from a blank file.
|
||||
|
||||
## Write it
|
||||
|
||||
```astro
|
||||
---
|
||||
interface Props {
|
||||
number: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
href: string;
|
||||
}
|
||||
const { number, title, summary, href } = Astro.props;
|
||||
---
|
||||
<article class="card">
|
||||
<b>{number}</b>
|
||||
<h2>{title}</h2>
|
||||
<p>{summary}</p>
|
||||
<a href={href}>Open chapter →</a>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card { display: grid; gap: 10px; padding: 22px; background: var(--paper); }
|
||||
b { color: var(--accent); font: var(--font-eyebrow); }
|
||||
</style>
|
||||
```
|
||||
|
||||
Rules that bite most often here:
|
||||
|
||||
- **No raw hex.** Tokens only. `check-tokens.mjs` will fail you.
|
||||
- **No `client:*`** unless it is genuinely interactive, and say why in the PR.
|
||||
- Keep the `gap:1px` over a coloured parent trick where the original used it —
|
||||
it is the house style, not a bug.
|
||||
- Preserve every ARIA attribute from the markup you are replacing.
|
||||
|
||||
## Prove it
|
||||
|
||||
1. Render it at 560 / 800 / 1100 / 1600 px.
|
||||
2. Tab to it. Focus ring visible (`outline: 3px solid #a7483f`).
|
||||
3. Diff its rendered text against the markup it replaced.
|
||||
4. Run [`../../checklists/before-component.md`](../../checklists/before-component.md).
|
||||
5. `npm run verify` — green, with no assertion deleted.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: astro-page
|
||||
description: Migrate one hand-written HTML page of the ai-for-dummies site to an Astro route without changing its URL, content, or JS budget. Use for any page-level migration task.
|
||||
---
|
||||
|
||||
# Migrating a page to Astro
|
||||
|
||||
## Snapshot first, migrate second
|
||||
|
||||
The snapshot is the only objective evidence that no content was lost.
|
||||
|
||||
```bash
|
||||
npm run serve & # vanilla site on :4173
|
||||
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ \
|
||||
> .agents/snapshots/models.txt
|
||||
```
|
||||
|
||||
Screenshot the same route at 560 / 800 / 1100 / 1600 px as well.
|
||||
|
||||
## Migrate
|
||||
|
||||
```bash
|
||||
cp .agents/templates/pages/chapter.astro src/pages/models.astro
|
||||
```
|
||||
|
||||
Then, in order:
|
||||
|
||||
1. Move markup into the layout + components. Reuse existing components before
|
||||
creating new ones.
|
||||
2. Move copy into `src/content/`. Both `en` and `pt`, copied literally — never
|
||||
retyped.
|
||||
3. Move page CSS into component `<style>` blocks. Do **not** port
|
||||
`responsive.css` wholesale; take what this page needs and prove the rest dead.
|
||||
4. Keep every `data-*` hook. `verify.mjs` asserts many of them by name
|
||||
(`data-phase`, `data-tree`, `data-route`, `data-model-provider`, …).
|
||||
5. Keep every ARIA attribute and the `<title>` / `<meta name="description">`.
|
||||
|
||||
## The URL must not change
|
||||
|
||||
Served from `/ai-for-dummies/`, so `base` is set in `astro.config.mjs`. Never
|
||||
hand-write an internal absolute path; use `import.meta.env.BASE_URL`.
|
||||
|
||||
Trailing slashes matter. `/models/` must not become `/models`.
|
||||
|
||||
If the page honours query params (the review desk uses `?author=`, `?skill=`,
|
||||
`?view=`, `?file=`, `?compare=`, `?render=`), they must still work — they are
|
||||
documented in the page footer and shared externally.
|
||||
|
||||
## Prove it
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
node .agents/scripts/snapshot-route.mjs dist/models/index.html > /tmp/after.txt
|
||||
diff .agents/snapshots/models.txt /tmp/after.txt # empty, or justify every line
|
||||
node scripts/audit-ui.mjs
|
||||
npm run verify
|
||||
```
|
||||
|
||||
Then screenshots at the same four widths, and
|
||||
[`../../checklists/before-page.md`](../../checklists/before-page.md).
|
||||
|
||||
## JS budget
|
||||
|
||||
A page that shipped zero JS must still ship zero. Check the build output. If
|
||||
your migration added a `client:load` to a static page, you did it wrong.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: content-migration
|
||||
description: Move bilingual copy out of app.js and catalog.js into typed Astro content collections without losing or altering a single string. Use for any task that relocates user-visible text.
|
||||
---
|
||||
|
||||
# Content migration
|
||||
|
||||
## What you are moving
|
||||
|
||||
- `app.js` — ~50 `{ en, pt }` keys across `phases`, `handsOnPrompts`,
|
||||
`modelGuide`, `skillSources`, `skillInstallPrompts`
|
||||
- `skills-review/catalog.js` + `submitted-catalog.js` — 24 entries with
|
||||
`id`, `author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`,
|
||||
`improved` (full markdown)
|
||||
|
||||
These are hand-written translations with deliberate tone. **Copy them. Never
|
||||
retype them.** Retyping introduces drift you will not notice.
|
||||
|
||||
## Schema
|
||||
|
||||
```ts
|
||||
// src/content/config.ts
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const localized = z.object({ en: z.string(), pt: z.string() });
|
||||
|
||||
const guide = defineCollection({
|
||||
type: 'data',
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
model: localized,
|
||||
title: localized,
|
||||
copy: localized,
|
||||
code: localized,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Both locales **required**. A missing `pt` must be a build error — silent English
|
||||
fallback is how a bilingual site quietly becomes monolingual.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. Extract the literals mechanically (script, not by hand).
|
||||
2. Write them into the collection.
|
||||
3. Diff extracted-before against extracted-after. Must be empty.
|
||||
|
||||
```bash
|
||||
node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json
|
||||
node .agents/scripts/extract-strings.mjs src/content/ > /tmp/after.json
|
||||
diff /tmp/before.json /tmp/after.json
|
||||
```
|
||||
|
||||
4. Only then delete the source literals.
|
||||
|
||||
## The generator trap
|
||||
|
||||
`skill-reviews/improved/**/SKILL.md` is **generated** from `catalog.js`
|
||||
`improved` fields by `scripts/build-skill-review.mjs`, and the output is
|
||||
committed to the repo. If you move `catalog.js`, that generator breaks silently
|
||||
— it will still run, just against nothing.
|
||||
|
||||
Either keep the generator pointed at the new collection, or replace it and
|
||||
update `package.json`, `README.md`, `docs/operations-guide.md`, and the review
|
||||
desk footer, all of which reference it.
|
||||
|
||||
## Markdown
|
||||
|
||||
The `improved` fields are full Markdown rendered client-side today. Move them to
|
||||
real `.md` files in the collection and let Astro render at build time. That
|
||||
deletes the hand-rolled renderer and improves fidelity — but re-check the review
|
||||
desk's diff view, which compares original and improved source text and needs the
|
||||
raw string, not just rendered HTML.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: design-tokens
|
||||
description: Extract, name, and enforce the design token layer for the ai-for-dummies site. Use when touching any colour, font size, spacing value, or breakpoint, when consolidating the three drifted palettes, or when a check-tokens failure needs resolving.
|
||||
---
|
||||
|
||||
# Design tokens
|
||||
|
||||
## Before anything
|
||||
|
||||
Read [`../../context/design-system.md`](../../context/design-system.md). This
|
||||
site has **three drifting palettes** and a **broken `@font-face`**. Both are
|
||||
traps. If you have not read that file, you will "preserve the styles" by
|
||||
copying a bug.
|
||||
|
||||
## Extracting
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
files=['styles.css','chapters.css','landing.css','rules/styles.css','skills/styles.css',
|
||||
'skills-review/styles.css','hands-on/starter/styles.css','hands-on/rules/styles.css']
|
||||
seen={}
|
||||
for f in files:
|
||||
for m in re.finditer(r'--([a-z-]+):\s*([^;}]+)', open(f).read()):
|
||||
seen.setdefault(m.group(1),{}).setdefault(m.group(2).strip(),[]).append(f)
|
||||
for k,v in sorted(seen.items()):
|
||||
print(f"--{k}:")
|
||||
for val,fs in v.items(): print(f" {val:12} <- {', '.join(fs)}")
|
||||
PY
|
||||
```
|
||||
|
||||
Re-run this after any consolidation. Every token should print exactly one value.
|
||||
|
||||
## Consolidating a drifted token
|
||||
|
||||
1. List every value and where it is used (above).
|
||||
2. Compute the perceptual delta. Sub-perceptual (a few units per channel) →
|
||||
canonicalize freely. Visible (`--blue`: `#527f9f` vs `#215675`) → screenshot
|
||||
both, get a human decision, record it in the task file.
|
||||
3. Pick the canonical value. Prefer the one used on the most-visited surface.
|
||||
4. Replace, then screenshot every affected page at 560/800/1100/1600 px.
|
||||
5. Attach before/after images to the task report. "It looked fine to me" is not
|
||||
evidence.
|
||||
|
||||
## Naming
|
||||
|
||||
Semantic, matching the existing vocabulary — `--ink`, `--paper`, `--muted`,
|
||||
`--line`, `--accent`, `--gold`, `--blue`, `--deep`. Never numeric scales. New
|
||||
surfaces extend semantically: `--surface-lab`, `--ink-inverse`.
|
||||
|
||||
## Type scale and breakpoints
|
||||
|
||||
Collapse the 14 ad-hoc `clamp()` triples to named steps and the 16 breakpoints
|
||||
to five, per [`../../rules/theming.md`](../../rules/theming.md). When collapsing
|
||||
a breakpoint, **screenshot at the old value** — that is where the regression is.
|
||||
|
||||
## Enforcing
|
||||
|
||||
```bash
|
||||
node .agents/scripts/check-tokens.mjs # fails on raw hex outside tokens.css
|
||||
```
|
||||
|
||||
Wire it into `npm run verify`. A rule nobody checks is a suggestion.
|
||||
|
||||
## The font decision
|
||||
|
||||
Do not self-host Manrope/DM Mono as part of a refactor task. The intended fonts
|
||||
have never rendered, so adopting them is a visual redesign. Default to matching
|
||||
what renders today (Arial / generic monospace) and delete the dead `@font-face`.
|
||||
If a human wants the real fonts, that is its own task with its own screenshots.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: motion
|
||||
description: Add or review animation on the ai-for-dummies site — transitions, state changes, view transitions. Use when any element moves, fades, or transforms, or when auditing existing motion for performance and reduced-motion support.
|
||||
---
|
||||
|
||||
# Motion
|
||||
|
||||
This is an editorial-print design. Motion is punctuation. Read
|
||||
[`../../rules/animation.md`](../../rules/animation.md) — it is binding; this is
|
||||
the procedure.
|
||||
|
||||
## Decide it should move
|
||||
|
||||
Answer out loud: **what does this motion tell the user that the static state
|
||||
does not?** Valid answers: something changed, attention needs directing to what
|
||||
changed, a layout shift needs smoothing. "It feels more polished" is not an
|
||||
answer — on this design it reads as generic.
|
||||
|
||||
If there is no answer, ship it static. That is a legitimate, common outcome.
|
||||
|
||||
## Build it
|
||||
|
||||
```css
|
||||
.panel {
|
||||
transition: opacity 180ms cubic-bezier(.2,0,0,1),
|
||||
transform 180ms cubic-bezier(.2,0,0,1);
|
||||
}
|
||||
.panel[data-state='entering'] { opacity: 0; transform: translateY(6px); }
|
||||
```
|
||||
|
||||
- **`transform` and `opacity` only.** Animating `width`/`height`/`top`/`left`
|
||||
forces layout every frame and shows up as a failed INP (budget: 200ms).
|
||||
- 150–250ms for UI feedback; up to 400ms for a page transition.
|
||||
- One thing moves at a time. No staggered card cascades.
|
||||
- `will-change` only immediately before animating, removed after.
|
||||
- No animation library. This site's thesis is having no runtime dependencies.
|
||||
|
||||
## Reduced motion is not optional
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation-duration: .01ms !important; animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important; scroll-behavior: auto !important; }
|
||||
}
|
||||
```
|
||||
|
||||
Then **test it**: DevTools → Rendering → Emulate `prefers-reduced-motion:
|
||||
reduce`. The end state must still be correct and the UI still usable. Reduced,
|
||||
not broken.
|
||||
|
||||
## Page transitions
|
||||
|
||||
Astro's `<ClientRouter />` is the only sanctioned motion dependency. Before
|
||||
enabling it, verify:
|
||||
|
||||
- JS disabled → full navigation still works
|
||||
- browser back/forward still works
|
||||
- the review desk's query-param deep links survive
|
||||
- `prefers-reduced-motion` is honoured
|
||||
|
||||
## Audit an existing animation
|
||||
|
||||
```bash
|
||||
grep -rn "transition\|animation\|@keyframes\|transform" src/ --include="*.astro" --include="*.css"
|
||||
```
|
||||
|
||||
For each hit ask: does it animate a layout property? does it respect reduced
|
||||
motion? is there a stated purpose? Three questions, three possible fixes.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: verify-contract
|
||||
description: Evolve scripts/verify.mjs across the Astro migration without losing coverage. Use whenever a verify assertion fails because of a refactor, or when adding checks for new architecture.
|
||||
---
|
||||
|
||||
# The verification contract
|
||||
|
||||
Read [`../../context/verification.md`](../../context/verification.md) first.
|
||||
|
||||
`verify.mjs` has 42 assertions pinning real content and interactions. They are
|
||||
the only thing preventing silent content loss during this migration, and they
|
||||
will all break, because they assert against files that stop existing.
|
||||
|
||||
## The rule
|
||||
|
||||
**A failing assertion is a question, not a bug to delete.**
|
||||
|
||||
```
|
||||
assertion fails
|
||||
↓
|
||||
does the user-visible fact it pins still exist?
|
||||
├── yes → re-point the assertion at the new location
|
||||
└── no → you deleted content. Put it back, or get sign-off.
|
||||
```
|
||||
|
||||
`grep -c 'throw new Error' scripts/verify.mjs` must not decrease. If it must,
|
||||
the `verification-engineer` writes a one-line reason per removal in the task
|
||||
report. Nobody else may reduce coverage.
|
||||
|
||||
## Translating assertions
|
||||
|
||||
| Kind | Old | New |
|
||||
| --- | --- | --- |
|
||||
| Content presence | `html.includes('data-phase="plan"')` | same token, read from `dist/full-guide/index.html` |
|
||||
| Implementation detail | `js.includes('renderTree')` | assert the rendered output has the tree UI, not that a function is named that |
|
||||
| Asset version | `'app.js?v=20260904-vote-widget'` | assert the built HTML references a hashed asset |
|
||||
|
||||
Implementation-detail assertions are the dangerous ones: they *look* deletable.
|
||||
They are pinning a feature. Replace with an output-level assertion of the same
|
||||
feature; never drop.
|
||||
|
||||
## Add the stronger check
|
||||
|
||||
Token matching cannot catch a dropped paragraph. Add rendered-text snapshots:
|
||||
|
||||
```bash
|
||||
# before migrating
|
||||
node .agents/scripts/snapshot-route.mjs http://localhost:4173/models/ > .agents/snapshots/models.txt
|
||||
# after
|
||||
node .agents/scripts/snapshot-route.mjs dist/models/index.html | diff .agents/snapshots/models.txt -
|
||||
```
|
||||
|
||||
Commit the snapshots. They are the migration's regression net.
|
||||
|
||||
## Extend audit-ui.mjs
|
||||
|
||||
It rejects external `<script>`/`<link>` but **misses external URLs inside CSS** —
|
||||
which is exactly how the broken Google Fonts `@font-face` in `styles.css:1` got
|
||||
into a "dependency-free" site. Add:
|
||||
|
||||
```js
|
||||
if (/@import|src:\s*url\(['"]?https?:|url\(['"]?https?:/i.test(css))
|
||||
throw new Error(`${file} has an external CSS dependency`);
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: visual-regression
|
||||
description: Prove a refactor did not change how the site looks. Use before and after any page migration, token consolidation, or breakpoint change on ai-for-dummies.
|
||||
---
|
||||
|
||||
# Visual regression
|
||||
|
||||
"Maintain the same styles" is a testable claim. Test it.
|
||||
|
||||
## Capture
|
||||
|
||||
The repo already has a Playwright pattern (`scripts/inspect.py`). Extend it
|
||||
rather than inventing one.
|
||||
|
||||
```python
|
||||
from playwright.sync_api import sync_playwright
|
||||
ROUTES = ['/', '/full-guide/', '/summary/', '/models/', '/agents/',
|
||||
'/skills/', '/rules/', '/skills-review/',
|
||||
'/hands-on/starter/', '/hands-on/rules/']
|
||||
WIDTHS = [560, 800, 1100, 1600]
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
for route in ROUTES:
|
||||
for w in WIDTHS:
|
||||
page = browser.new_page(viewport={'width': w, 'height': 900})
|
||||
page.goto(f'{BASE}{route}', wait_until='networkidle')
|
||||
page.screenshot(path=f'{OUT}/{route.strip("/").replace("/","_") or "index"}-{w}.png',
|
||||
full_page=True)
|
||||
page.close()
|
||||
browser.close()
|
||||
```
|
||||
|
||||
Run once against the vanilla site (`npm run serve`), once against
|
||||
`npm run preview`. Keep both sets.
|
||||
|
||||
## Compare
|
||||
|
||||
```bash
|
||||
for f in before/*.png; do
|
||||
compare -metric AE "$f" "after/$(basename $f)" null: 2>&1 # ImageMagick
|
||||
echo " <- $(basename $f)"
|
||||
done
|
||||
```
|
||||
|
||||
Pixel-exact is not the bar — antialiasing differs. Judge by eye where the
|
||||
metric is non-trivial, and attach the pair to the task report.
|
||||
|
||||
## The three widths that catch the most
|
||||
|
||||
- **560px** — where the 16 ad-hoc breakpoints collapse to `--bp-sm`. Highest
|
||||
risk in the whole migration.
|
||||
- **800px** — the most common existing breakpoint; layout flips here.
|
||||
- **1600px** — `min-width` rules that only fire on large screens are the ones
|
||||
nobody notices are broken.
|
||||
|
||||
Also screenshot at the **old** breakpoint values you removed (520, 530, 600,
|
||||
620, 720, 850, 880, 900), not just the new ones. Regressions hide exactly there.
|
||||
|
||||
## What a real difference looks like
|
||||
|
||||
Expect and accept: sub-pixel text shifts, antialiasing.
|
||||
|
||||
Investigate: anything that moves by more than ~2px, any colour change (that is
|
||||
a token bug), any element that appears or disappears (that is content loss —
|
||||
stop and check the snapshot diff).
|
||||
|
||||
## Reduced motion
|
||||
|
||||
Capture one pass with `prefers_reduced_motion='reduce'`. Animations must land in
|
||||
their correct end state, not vanish.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
// Grid-group template — for a set of sibling cards separated by hairlines.
|
||||
//
|
||||
// Note the separator technique: `gap: 1px` over a coloured parent background.
|
||||
// That is DELIBERATE house style throughout this site, not a workaround.
|
||||
// Do not "fix" it into `border`.
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
/** Number of columns at the widest breakpoint. */
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
const { label, columns = 3 } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="group" aria-label={label}>
|
||||
<div class="grid" style={`--columns: ${columns}`}>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns), 1fr);
|
||||
gap: 1px; /* hairline separators, drawn by the parent background */
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
/* Children paint their own background, which is what makes the 1px show. */
|
||||
.grid > :global(*) {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
// Interactive island template. Use ONLY when the component genuinely needs
|
||||
// client-side behaviour, and write the justification in your PR description.
|
||||
//
|
||||
// Islands are LEAVES. Do not wrap static children that could have been
|
||||
// server-rendered — hydrate the tab panel, not the page.
|
||||
//
|
||||
// Hydration preference, in order:
|
||||
// (none) → client:visible → client:idle → client:load
|
||||
//
|
||||
// Usage: <Island client:visible items={items} />
|
||||
|
||||
interface Props {
|
||||
items: { id: string; label: string; body: string }[];
|
||||
initialId?: string;
|
||||
}
|
||||
|
||||
const { items, initialId = items[0]?.id } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="island" data-initial={initialId}>
|
||||
<div class="tabs" role="tablist" aria-label="Sections">
|
||||
{items.map((item) => (
|
||||
<button
|
||||
role="tab"
|
||||
id={`tab-${item.id}`}
|
||||
aria-controls={`panel-${item.id}`}
|
||||
aria-selected={item.id === initialId}
|
||||
data-tab={item.id}
|
||||
>{item.label}</button>
|
||||
))}
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`panel-${item.id}`}
|
||||
aria-labelledby={`tab-${item.id}`}
|
||||
data-panel={item.id}
|
||||
hidden={item.id !== initialId}
|
||||
>{item.body}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Scoped to this island's own root so multiple instances never collide.
|
||||
document.querySelectorAll<HTMLElement>('.island').forEach((root) => {
|
||||
const tabs = root.querySelectorAll<HTMLButtonElement>('[data-tab]');
|
||||
const select = (id: string) => {
|
||||
tabs.forEach((tab) => tab.setAttribute('aria-selected', String(tab.dataset.tab === id)));
|
||||
root.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {
|
||||
panel.hidden = panel.dataset.panel !== id;
|
||||
});
|
||||
};
|
||||
tabs.forEach((tab) => tab.addEventListener('click', () => select(tab.dataset.tab!)));
|
||||
|
||||
// Arrow-key navigation is required for role="tablist" — see
|
||||
// .agents/rules/accessibility.md
|
||||
root.querySelector('[role="tablist"]')?.addEventListener('keydown', (event) => {
|
||||
const key = (event as KeyboardEvent).key;
|
||||
if (key !== 'ArrowRight' && key !== 'ArrowLeft') return;
|
||||
const list = [...tabs];
|
||||
const current = list.findIndex((tab) => tab.getAttribute('aria-selected') === 'true');
|
||||
const next = list[(current + (key === 'ArrowRight' ? 1 : -1) + list.length) % list.length];
|
||||
select(next.dataset.tab!);
|
||||
next.focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tabs { display: grid; gap: 8px; }
|
||||
|
||||
button {
|
||||
padding: 12px;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
border: 1px solid var(--line);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
/* transform/opacity only — never animate layout properties */
|
||||
transition: background 180ms cubic-bezier(.2, 0, 0, 1);
|
||||
}
|
||||
|
||||
button[aria-selected='true'] {
|
||||
color: var(--paper);
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
button { transition-duration: .01ms; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
// Static block template — the default. Ships zero JavaScript.
|
||||
// Copy to src/components/blocks/<Name>.astro and replace everything marked TODO.
|
||||
//
|
||||
// Before using this, confirm the block earns extraction: it appears three times,
|
||||
// or it has a name a person says out loud. See .agents/rules/componentization.md
|
||||
|
||||
interface Props {
|
||||
/** TODO: describe each prop. Required by default; optional needs a reason. */
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
body: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const { eyebrow, title, body, href } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="block">
|
||||
<span class="eyebrow">{eyebrow}</span>
|
||||
<h2>{title}</h2>
|
||||
<p>{body}</p>
|
||||
{href && <a href={href}>Open →</a>}
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* Tokens only. No raw hex, no px font sizes, no ad-hoc breakpoints.
|
||||
.agents/scripts/check-tokens.mjs enforces this. */
|
||||
.block {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 22px;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* The house eyebrow: uppercase monospace, wide tracking. One class, not
|
||||
fifteen repetitions. */
|
||||
.eyebrow {
|
||||
color: var(--accent);
|
||||
font: var(--font-eyebrow);
|
||||
letter-spacing: .1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--step-5);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -.06em; /* tight display tracking is a signature of this design */
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--blue);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 3px solid var(--red);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.block { padding: 18px; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
dist
|
||||
node_modules
|
||||
public/hands-on
|
||||
submitted-skills
|
||||
skill-reviews
|
||||
vote-service
|
||||
.agents/snapshots
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
"overrides": [
|
||||
{ "files": "*.astro", "options": { "parser": "astro" } },
|
||||
{ "files": "*.md", "options": { "proseWrap": "always", "printWidth": 80 } }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"ignoreFiles": [
|
||||
"dist/**",
|
||||
"public/hands-on/**",
|
||||
"submitted-skills/**"
|
||||
],
|
||||
"rules": {
|
||||
"custom-property-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
|
||||
"declaration-property-value-disallowed-list": {
|
||||
"/^transition/": ["/width/", "/height/", "/^top/", "/^left/", "/margin/"],
|
||||
"/^animation/": ["/width/", "/height/"]
|
||||
},
|
||||
"media-feature-name-no-unknown": true,
|
||||
"no-descending-specificity": null,
|
||||
"selector-class-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
|
||||
},
|
||||
"_comments": {
|
||||
"declaration-property-value-disallowed-list": "Animating layout properties forces reflow every frame and fails the 200ms INP budget. transform and opacity only — see .agents/rules/animation.md",
|
||||
"ignoreFiles": "hands-on/ is a lab fixture and submitted-skills/ is other people's work; neither is ours to restyle"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Flat config (ESLint 9+). Copy to the repository root in task 01.
|
||||
import js from '@eslint/js';
|
||||
import astro from 'eslint-plugin-astro';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...astro.configs.recommended,
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'public/hands-on/**', // lab fixtures ship verbatim — linting them would
|
||||
// invite "fixes" that break the exercise
|
||||
'submitted-skills/**', // other people's work, reproduced as submitted
|
||||
'skill-reviews/**', // generated from skills-review/catalog.js
|
||||
'vote-service/**', // Go service, separate lifecycle
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
eqeqeq: ['error', 'always'],
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,39 @@
|
||||
# Tier 3 gate. Copy to .gitea/workflows/verify.yml in task 01.
|
||||
#
|
||||
# Known trap: this Gitea's act-runner registration lives in an emptyDir, so a
|
||||
# pod restart silently kills CI. If the site stops updating, check the runner
|
||||
# BEFORE debugging the workflow.
|
||||
name: verify
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # gate.sh compares assertion counts against origin/main
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- run: npm ci --prefer-offline
|
||||
- run: npm run lint
|
||||
- run: ./.agents/scripts/gate.sh
|
||||
|
||||
# Tier 3 only: too slow for pre-push, essential before publishing.
|
||||
- name: visual regression
|
||||
run: |
|
||||
npx playwright install --with-deps chromium
|
||||
node .agents/scripts/visual-regression.mjs
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: .agents/snapshots/diff/
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"_note": "Merge these into package.json in task 01. `prepare` is what installs husky; without it every hook is inert.",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"lint": "eslint . --max-warnings=0 && stylelint '**/*.css' --max-warnings=0",
|
||||
"format": "prettier --write .",
|
||||
"verify": "node scripts/verify.mjs && node scripts/audit-ui.mjs && node .agents/scripts/check-tokens.mjs",
|
||||
"gate": "./.agents/scripts/gate.sh",
|
||||
"snapshot": "node .agents/scripts/snapshot-route.mjs",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^5",
|
||||
"@eslint/js": "^9",
|
||||
"eslint": "^9",
|
||||
"eslint-plugin-astro": "^1",
|
||||
"husky": "^9",
|
||||
"lint-staged": "^16",
|
||||
"prettier": "^3",
|
||||
"prettier-plugin-astro": "^0.14",
|
||||
"stylelint": "^16",
|
||||
"stylelint-config-standard": "^39"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
// Chapter page template — for the numbered chapters (models, agents, skills,
|
||||
// rules). These ship ZERO JavaScript today and must continue to.
|
||||
//
|
||||
// Copy to src/pages/<slug>.astro. The route must match the existing URL
|
||||
// exactly, trailing slash included.
|
||||
|
||||
import ChapterLayout from '../layouts/ChapterLayout.astro';
|
||||
import GridGroup from '../components/blocks/GridGroup.astro';
|
||||
import StaticBlock from '../components/blocks/StaticBlock.astro';
|
||||
import { getEntry } from 'astro:content';
|
||||
|
||||
// Content comes from a collection, never hard-coded in the page.
|
||||
// Both `en` and `pt` are required by the schema.
|
||||
const chapter = await getEntry('chapters', 'models');
|
||||
const lang = 'en'; // TODO: wire to the language toggle decision (task 03)
|
||||
---
|
||||
|
||||
<ChapterLayout
|
||||
number={chapter.data.number}
|
||||
title={chapter.data.title[lang]}
|
||||
description={chapter.data.description[lang]}
|
||||
>
|
||||
<section class="hero">
|
||||
<p class="eyebrow">{chapter.data.eyebrow[lang]}</p>
|
||||
<h1 set:html={chapter.data.heading[lang]} />
|
||||
<p class="lede">{chapter.data.lede[lang]}</p>
|
||||
</section>
|
||||
|
||||
<GridGroup label="Chapter sections" columns={3}>
|
||||
{chapter.data.sections.map((section) => (
|
||||
<StaticBlock
|
||||
eyebrow={section.eyebrow[lang]}
|
||||
title={section.title[lang]}
|
||||
body={section.body[lang]}
|
||||
/>
|
||||
))}
|
||||
</GridGroup>
|
||||
</ChapterLayout>
|
||||
|
||||
<style>
|
||||
/* Page-level layout only. Anything reusable belongs in a component. */
|
||||
.hero { max-width: 780px; padding: clamp(75px, 12vh, 145px) 0 85px; }
|
||||
|
||||
h1 {
|
||||
margin: 16px 0 24px;
|
||||
font-size: var(--step-display);
|
||||
line-height: .86;
|
||||
letter-spacing: -.08em;
|
||||
}
|
||||
|
||||
/* Georgia is a real system font and DOES render — unlike Manrope/DM Mono.
|
||||
See .agents/context/design-system.md */
|
||||
h1 :global(em) {
|
||||
color: var(--blue);
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.lede { max-width: 570px; color: var(--muted); line-height: 1.65; }
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
// Interactive page template — for pages that genuinely need client-side
|
||||
// behaviour (the full guide, the review desk).
|
||||
//
|
||||
// The page itself is still server-rendered. Only the islands hydrate.
|
||||
// If you are copying this for a page that has no interaction, use
|
||||
// chapter.astro instead.
|
||||
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Island from '../components/islands/Island.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const entries = await getCollection('guide');
|
||||
const lang = 'en'; // TODO: wire to the language toggle decision (task 03)
|
||||
|
||||
const items = entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.data.title[lang],
|
||||
body: entry.data.copy[lang],
|
||||
}));
|
||||
---
|
||||
|
||||
<BaseLayout title="Full field guide" description="TODO">
|
||||
<!-- Static content is server-rendered. No hydration cost. -->
|
||||
<section class="hero">
|
||||
<p class="eyebrow">The full guide</p>
|
||||
<h1>Ship the <em>system.</em></h1>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
client:visible, not client:load — this is below the fold and the page must
|
||||
stay interactive-free until it matters. Justification belongs in the PR:
|
||||
"tab panel requires click-driven state; no server equivalent."
|
||||
-->
|
||||
<Island client:visible items={items} />
|
||||
|
||||
<!-- More static content after the island. Islands are leaves, not wrappers. -->
|
||||
<slot />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { max-width: 780px; padding: clamp(75px, 12vh, 145px) 0 85px; }
|
||||
h1 { font-size: var(--step-display); line-height: .86; letter-spacing: -.08em; }
|
||||
h1 em { color: var(--blue); font-family: Georgia, serif; font-weight: 400; }
|
||||
</style>
|
||||
+11
@@ -1,3 +1,14 @@
|
||||
# Tooling caches, not part of the published site.
|
||||
.serena/
|
||||
__pycache__/
|
||||
|
||||
# Build + toolchain (Astro migration).
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.husky/_/
|
||||
|
||||
# Visual-regression working output. Baselines in .agents/snapshots/ ARE tracked;
|
||||
# the per-run captures and diffs are not.
|
||||
.agents/snapshots/after/
|
||||
.agents/snapshots/diff/
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Conventional commits, matching this repository's existing log
|
||||
# (feat:, fix:, docs:, refactor:, chore:, test:, style:, ci:).
|
||||
#
|
||||
# Deliberately a plain regex rather than a commitlint dependency: this project's
|
||||
# thesis is having no unnecessary dependencies, and one regex is legible to
|
||||
# every agent that has to satisfy it.
|
||||
|
||||
message_file=$1
|
||||
first_line=$(head -n1 "$message_file")
|
||||
|
||||
# Allow merge and revert commits through untouched.
|
||||
case "$first_line" in
|
||||
"Merge "*|"Revert "*) exit 0 ;;
|
||||
esac
|
||||
|
||||
if ! printf '%s' "$first_line" | grep -qE '^(feat|fix|docs|refactor|chore|test|style|ci|perf)(\([a-z0-9 -]+\))?: .{1,}$'; then
|
||||
echo "commit-msg: subject must be '<type>: <subject>' (lowercase type)." >&2
|
||||
echo " types: feat fix docs refactor chore test style ci perf" >&2
|
||||
echo " got: $first_line" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${#first_line}" -gt 72 ]; then
|
||||
echo "commit-msg: subject is ${#first_line} chars; keep it under 72." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,10 @@
|
||||
# Tier 1: staged files only. Budget < 3s — see .agents/rules/gates.md
|
||||
#
|
||||
# Husky v9+: no `husky.sh` sourcing (deprecated in v9, removed in v10).
|
||||
|
||||
# Guard the silent-failure mode: if hooks are configured but .husky/_ is
|
||||
# missing (fresh worktree that never ran `npm install`), this file would not
|
||||
# execute at all. Nothing we can do from inside it — so verify-hooks.sh exists
|
||||
# and worktree.sh runs it at creation time.
|
||||
|
||||
npx --no-install lint-staged
|
||||
@@ -0,0 +1,4 @@
|
||||
# Tier 2: the real gate. Whole project. Budget < 90s.
|
||||
# Takes a cross-worktree lock so parallel agents queue instead of thrashing.
|
||||
|
||||
exec .agents/scripts/gate.sh
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"*.{js,mjs,ts,astro}": [
|
||||
"prettier --write",
|
||||
"eslint --fix --max-warnings=0"
|
||||
],
|
||||
"*.css": [
|
||||
"prettier --write",
|
||||
"stylelint --fix --max-warnings=0"
|
||||
],
|
||||
"*.{astro,css}": [
|
||||
"node .agents/scripts/check-tokens.mjs"
|
||||
],
|
||||
"*.{json,md,yml,yaml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# AGENTS.md
|
||||
|
||||
Entry point for any AI agent or new engineer working in this repository. Read
|
||||
this file, then follow the links you need. Full map:
|
||||
[`.agents/ORCHESTRATOR.md`](.agents/ORCHESTRATOR.md).
|
||||
|
||||
## What is this project?
|
||||
|
||||
**"AI For Dummies" — a bilingual (EN/PT-BR) presentation and workshop about
|
||||
working with coding agents: model routing, subagents, git worktrees, skills,
|
||||
rules, and verification.** It is published as a static site on a self-hosted
|
||||
Gitea Pages Server, and it doubles as its own teaching artifact: the hands-on
|
||||
labs are dependency-free HTML/CSS/JS that workshop attendees point an agent at.
|
||||
|
||||
- **Current stack**: hand-written HTML + CSS + ES modules, no build step, no dependencies
|
||||
- **Target stack**: Astro (see [`plans/astro-refactor/`](plans/astro-refactor/README.md)) — migration in progress
|
||||
- **Languages**: English and Brazilian Portuguese, toggled client-side
|
||||
- **Companion service**: `vote-service/` (Go + Kubernetes) — separate lifecycle, see its own README
|
||||
|
||||
## Essential commands
|
||||
|
||||
```bash
|
||||
npm run verify # content + interaction contracts (scripts/verify.mjs) — the gate
|
||||
node scripts/audit-ui.mjs # responsive / no-external-dependency audit
|
||||
node scripts/build-skill-review.mjs # regenerate skill-reviews/improved/ from catalog.js
|
||||
npm run serve # python3 -m http.server 4173
|
||||
```
|
||||
|
||||
`npm run verify` is not a formality. It is a set of ~42 string-token assertions
|
||||
that pin the site's real content and interactions. **A refactor that "passes"
|
||||
by deleting assertions has failed.** See
|
||||
[`.agents/context/verification.md`](.agents/context/verification.md).
|
||||
|
||||
## Publishing
|
||||
|
||||
`main` is the source of truth. The `pages` branch is what the Gitea Pages
|
||||
Server actually serves, and its tree must end up identical to `main`'s. The
|
||||
full procedure — including why `merge --ff-only` does *not* work here — is in
|
||||
[`docs/operations-guide.md`](docs/operations-guide.md).
|
||||
|
||||
Adding a build step changes this contract. Read
|
||||
[`.agents/context/publishing.md`](.agents/context/publishing.md) before doing so.
|
||||
|
||||
## Never touch
|
||||
|
||||
- `hands-on/starter/` and `hands-on/rules/` — **lab fixtures.** The exercise *is*
|
||||
that they are dependency-free vanilla HTML/CSS/JS an attendee can hand to an
|
||||
agent. Componentizing them destroys the lesson. They ship as static assets.
|
||||
- `submitted-skills/` — other people's submitted work, reproduced verbatim
|
||||
- `skill-reviews/improved/` — generated; edit `skills-review/catalog.js` instead
|
||||
- `vote-service/` — separate deploy lifecycle; do not fold into the site build
|
||||
- `package-lock.json`, `dist/`, `node_modules/`
|
||||
|
||||
## Rules
|
||||
|
||||
Binding. Read the one that covers what you are about to do.
|
||||
|
||||
| Rule | When |
|
||||
| --- | --- |
|
||||
| [`astro.md`](.agents/rules/astro.md) | any `.astro` file |
|
||||
| [`theming.md`](.agents/rules/theming.md) | any colour, font, or spacing value |
|
||||
| [`componentization.md`](.agents/rules/componentization.md) | creating or splitting a component |
|
||||
| [`animation.md`](.agents/rules/animation.md) | any motion, transition, or transform |
|
||||
| [`accessibility.md`](.agents/rules/accessibility.md) | any interactive element |
|
||||
| [`content-i18n.md`](.agents/rules/content-i18n.md) | any user-visible string |
|
||||
| [`code-style.md`](.agents/rules/code-style.md) | always |
|
||||
| [`git-worktrees.md`](.agents/rules/git-worktrees.md) | starting or finishing a task |
|
||||
|
||||
## The one thing to understand first
|
||||
|
||||
This site's styling is **not** a design system today. Three near-duplicate
|
||||
palettes have drifted apart (`--ink` exists as `#172f42`, `#122534`, and
|
||||
`#173044`), and the `@font-face` rule in `styles.css:1` is malformed, so the
|
||||
intended Manrope/DM Mono typography has never actually rendered. "Maintain the
|
||||
same styles" therefore needs a deliberate decision, not a copy-paste. Read
|
||||
[`.agents/context/design-system.md`](.agents/context/design-system.md) before
|
||||
touching any CSS.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Which model runs which task
|
||||
|
||||
You have **MiniMax-M3** (primary), **Gemini**, and **Codex**. They are not
|
||||
interchangeable here, and the split is not about which is "smartest" — it is
|
||||
about which failure mode each task punishes.
|
||||
|
||||
**Honest caveat up front:** I have not benchmarked these three on this
|
||||
repository. The routing below is reasoned from task shape and each model's
|
||||
known strengths. Validate it cheaply on **task 07** (small, self-contained,
|
||||
easy to judge) before fanning out across ten worktrees.
|
||||
|
||||
## Short answer
|
||||
|
||||
**Run the refactor on MiniMax-M3.** It is your primary, the work is mostly
|
||||
bounded mechanical migration with a hard verification gate, and that is exactly
|
||||
the shape M3 handles well at low cost. Reach for the other two at three specific
|
||||
points where M3 is the wrong tool.
|
||||
|
||||
## Routing table
|
||||
|
||||
| Task | Model | Why this one |
|
||||
| --- | --- | --- |
|
||||
| 01 scaffold + gates | **Codex** | Config-heavy, many interacting tools (Astro + husky + lint-staged + CI), and success is binary — it builds and hooks fire, or not. Codex's long autonomous run-until-green loop suits it, and getting the foundation wrong is expensive later. |
|
||||
| 02 design tokens | **Gemini** | Needs the whole CSS corpus in one context (8 stylesheets, ~90 KB) plus **visual judgement on screenshots**. Gemini's long context and multimodal comparison are the differentiator; the others would work file-by-file and miss cross-file drift. |
|
||||
| 03 verification net | **Codex** | Writing test tooling with a tight feedback loop. Precision about what an assertion pins matters more than speed. |
|
||||
| 04 content schema | **MiniMax-M3** | Small, well-specified, one file. |
|
||||
| 05–06 content migration | **MiniMax-M3** | High-volume mechanical string moves with a `diff` as the oracle. Cheap, parallel, verifiable. Exactly M3's sweet spot. |
|
||||
| 07 primitives | **MiniMax-M3** | Small components from templates. Use this task to calibrate the whole routing decision. |
|
||||
| 08–11 component blocks | **MiniMax-M3 ×4 parallel** | Four bounded tasks, one template each, checklist-gated. Cost per task matters because there are many. |
|
||||
| 12–14, 17 pages | **MiniMax-M3** | Bounded, snapshot-diff verified. |
|
||||
| 15 full guide | **Codex** | The hard one: 50 KB `app.js`, 12 render functions, tab state, bilingual swap. Long sustained reasoning over interacting pieces; the task most likely to need many iterations against a failing check. |
|
||||
| 16 review desk | **Codex** | Same shape and worse — search, filtering, file fetching, six query params, markdown rendering, client-side diff. Highest defect risk in the plan. |
|
||||
| 18 motion | **Gemini** | Judging whether motion looks right is perceptual. Feed it before/after captures. |
|
||||
| 19 contract re-point | **Codex** | 42 assertions to translate without losing coverage. Meticulous, mechanical, verifiable. |
|
||||
| 20 cutover | **you, with M3 assisting** | Touches production publishing. A human should be watching. |
|
||||
| review of every task | **Gemini** | Fresh eyes, whole-diff-in-context, and it did not write the code. Never review with the model that wrote it. |
|
||||
|
||||
## The reasoning in one line each
|
||||
|
||||
- **MiniMax-M3** — cheapest per task and strong at bounded, tool-driven edits.
|
||||
Use it for volume: 13 of the 20 tasks. Its weakness is long multi-file
|
||||
reasoning where the spec is vague; every task above that it owns has a
|
||||
template and a mechanical oracle.
|
||||
- **Codex** — best at "keep iterating until the check passes" over a
|
||||
complicated existing codebase. Use it where the loop is long and the answer is
|
||||
not obvious: scaffold, the two hard pages, verification.
|
||||
- **Gemini** — biggest context and genuinely useful multimodal comparison. Use
|
||||
it where the input is *everything at once* or where the judgement is
|
||||
**visual**: token consolidation, motion, screenshot diffing, and code review.
|
||||
|
||||
## Cross-checking rule
|
||||
|
||||
Whatever writes a task must not review it. Pair them: M3 writes → Gemini
|
||||
reviews; Codex writes → Gemini reviews; Gemini writes → Codex reviews. The
|
||||
`reviewer` agent definition is model-agnostic on purpose.
|
||||
|
||||
## Swap the routing if you see this
|
||||
|
||||
| Symptom | Move the task to |
|
||||
| --- | --- |
|
||||
| M3 spends more than ~3 iterations failing the same gate | Codex |
|
||||
| M3 edits files outside its task scope | Codex, and tighten the brief |
|
||||
| Codex "fixes" a red suite by deleting assertions | anything — but re-read `context/verification.md` to it first; `gate.sh` blocks the merge either way |
|
||||
| Gemini gives confident visual sign-off with no screenshots attached | require the artifacts; do not accept prose |
|
||||
| A task needs more than two models' worth of hand-holding | the task is too big — split it |
|
||||
|
||||
## Cost shape
|
||||
|
||||
Tasks 05–14 are ~two-thirds of the work and are all M3-eligible. Running those
|
||||
on Codex would work and cost several times more for no measurable quality gain,
|
||||
because the checklist and `gate.sh` — not the model — are what guarantee those
|
||||
outputs. Spend the expensive models where there is no mechanical oracle: 15, 16,
|
||||
and the visual judgement calls.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Astro refactor — plan
|
||||
|
||||
**Status: not started. These are briefs, not work.** Nothing in this plan has
|
||||
been implemented.
|
||||
|
||||
Goal: move `ai-for-dummies` from ten hand-written HTML pages to Astro, so that
|
||||
adding a chapter is a component and a content entry rather than a copy-pasted
|
||||
file — **without changing how the site looks, what it says, or what it costs a
|
||||
visitor to load.**
|
||||
|
||||
## Read before starting anything
|
||||
|
||||
| File | Why |
|
||||
| --- | --- |
|
||||
| [`../../AGENTS.md`](../../AGENTS.md) | entry point |
|
||||
| [`../../.agents/context/design-system.md`](../../.agents/context/design-system.md) | three drifting palettes, a font that has never rendered |
|
||||
| [`../../.agents/context/verification.md`](../../.agents/context/verification.md) | 42 assertions that will all break, and must not be deleted |
|
||||
| [`../../.agents/context/publishing.md`](../../.agents/context/publishing.md) | Gitea Pages serves a branch and cannot build |
|
||||
|
||||
## The three things most likely to go wrong
|
||||
|
||||
1. **Content loss that nobody notices.** 50 KB of bilingual copy moves between
|
||||
files. Snapshot every route *before* migrating it — task 03 exists to make
|
||||
that possible and blocks all page work.
|
||||
2. **Assertions deleted to make a red suite green.** That converts a content-loss
|
||||
bug into a passing build. `gate.sh` refuses a coverage drop.
|
||||
3. **Base-path bugs.** The site lives at `/ai-for-dummies/`. It will work
|
||||
perfectly in `npm run preview` and 404 in production. Verify on the real host.
|
||||
|
||||
## Phases
|
||||
|
||||
```
|
||||
Phase 0 foundation 01 → (02 ∥ 03 ∥ 04)
|
||||
Phase 1 content 05 ∥ 06 after 04
|
||||
Phase 2 components 07 → (08 ∥ 09 ∥ 10 ∥ 11) after 02
|
||||
Phase 3 pages 12 ∥ 13 ∥ 14 ∥ 17, then 15 ∥ 16
|
||||
Phase 4 polish 18 ∥ 19, then 20
|
||||
```
|
||||
|
||||
| # | Task | Agent | Depends on | Parallel with |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 01 | [scaffold + gates](task-01-scaffold.md) | astro-architect | — | — |
|
||||
| 02 | [design tokens](task-02-tokens.md) | design-system-keeper | 01 | 03, 04 |
|
||||
| 03 | [verification net](task-03-verification-net.md) | verification-engineer | 01 | 02, 04 |
|
||||
| 04 | [content schema](task-04-content-schema.md) | content-i18n-migrator | 01 | 02, 03 |
|
||||
| 05 | [guide content](task-05-content-guide.md) | content-i18n-migrator | 04 | 06 |
|
||||
| 06 | [review-desk content](task-06-content-review.md) | content-i18n-migrator | 04 | 05 |
|
||||
| 07 | [primitives](task-07-primitives.md) | component-builder | 02 | — |
|
||||
| 08 | [route cards](task-08-route-cards.md) | component-builder | 07 | 09, 10, 11 |
|
||||
| 09 | [chapter blocks](task-09-chapter-blocks.md) | component-builder | 07 | 08, 10, 11 |
|
||||
| 10 | [guide blocks](task-10-guide-blocks.md) | component-builder | 07 | 08, 09, 11 |
|
||||
| 11 | [review-desk blocks](task-11-review-blocks.md) | component-builder | 07 | 08, 09, 10 |
|
||||
| 12 | [landing page](task-12-page-landing.md) | page-migrator | 03, 08 | 13, 14, 17 |
|
||||
| 13 | [chapter pages ×4](task-13-page-chapters.md) | page-migrator | 03, 09 | 12, 14, 17 |
|
||||
| 14 | [rules page](task-14-page-rules.md) | page-migrator | 03, 09 | 12, 13, 17 |
|
||||
| 15 | [full guide](task-15-page-full-guide.md) | page-migrator | 05, 10, 13 | 16 |
|
||||
| 16 | [review desk](task-16-page-review-desk.md) | page-migrator | 06, 11, 13 | 15 |
|
||||
| 17 | [hands-on passthrough](task-17-hands-on.md) | astro-architect | 01 | 12, 13, 14 |
|
||||
| 18 | [motion pass](task-18-motion.md) | motion-designer | 15, 16 | 19 |
|
||||
| 19 | [contract re-point](task-19-verify-repoint.md) | verification-engineer | 15, 16 | 18 |
|
||||
| 20 | [cutover + cleanup](task-20-cutover.md) | astro-architect | all | — |
|
||||
|
||||
Widest parallelism: **four agents** (tasks 08–11, then 12/13/14/17). More than
|
||||
that and they start contending on review capacity, not on files.
|
||||
|
||||
## Running a task
|
||||
|
||||
```bash
|
||||
.agents/scripts/worktree.sh start 08 route-cards
|
||||
cd ../af-task-08
|
||||
# agent reads: plans/astro-refactor/task-08-route-cards.md
|
||||
# .agents/agents/component-builder.md (+ the skills it names)
|
||||
```
|
||||
|
||||
The script runs `npm ci` and `verify-hooks.sh` for you. That matters: `.husky/_`
|
||||
is generated, not committed, so a hand-made worktree has hooks configured but
|
||||
**silently not running**.
|
||||
|
||||
Finishing:
|
||||
|
||||
```bash
|
||||
npm run gate # tier 2, same as pre-push
|
||||
# reviewer agent reads the diff against .agents/checklists/before-merge.md
|
||||
.agents/scripts/worktree.sh finish 08 route-cards
|
||||
```
|
||||
|
||||
## Which model to run each task
|
||||
|
||||
See [`MODEL-ROUTING.md`](MODEL-ROUTING.md).
|
||||
|
||||
## These files must be committed
|
||||
|
||||
Worktrees check out tracked files. If this plan stays untracked, every worktree
|
||||
you create will be missing it. Commit `plans/` and `.agents/` before fanning out.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Task 01 — Astro scaffold, gates, and the publishing decision
|
||||
|
||||
**Agent**: `astro-architect` · **Model**: Codex · **Depends on**: nothing
|
||||
**Blocks**: everything · **Worktree**: `.agents/scripts/worktree.sh start 01 scaffold`
|
||||
|
||||
## Goal
|
||||
|
||||
A working Astro project that builds, serves one page correctly from the real
|
||||
host under `/ai-for-dummies/`, and has all three gate tiers live.
|
||||
|
||||
## Scope
|
||||
|
||||
`astro.config.mjs`, `package.json`, `tsconfig.json`, `src/layouts/BaseLayout.astro`,
|
||||
`.husky/`, `.lintstagedrc.json`, lint configs, `.gitea/workflows/verify.yml`,
|
||||
`docs/operations-guide.md`.
|
||||
|
||||
Copy the configs from `.agents/templates/config/` — they are written for this
|
||||
project (correct ignores for `hands-on/`, `submitted-skills/`, `vote-service/`).
|
||||
|
||||
## Steps
|
||||
|
||||
1. `npm create astro@latest` into the worktree — minimal template, TypeScript
|
||||
strict, **no UI framework, no CSS framework**.
|
||||
2. Set `base: '/ai-for-dummies'`. Every internal link goes through
|
||||
`import.meta.env.BASE_URL` from here on.
|
||||
3. `src/layouts/BaseLayout.astro`: `<html lang>`, viewport meta, title,
|
||||
description, global styles slot. Nothing clever.
|
||||
4. Merge `.agents/templates/config/package.scripts.json` into `package.json`.
|
||||
**`"prepare": "husky"` is what makes hooks exist** — without it every hook is
|
||||
inert.
|
||||
5. `npm install husky lint-staged prettier eslint stylelint …`, then `npx husky init`.
|
||||
6. Copy the three hooks and the lint configs into place. Verify with
|
||||
`.agents/scripts/verify-hooks.sh`.
|
||||
7. Migrate **one** page (`summary/` — smallest, zero JS) as a smoke test.
|
||||
8. Copy `hands-on/` into `public/` (task 17 does this properly; a stub is fine here).
|
||||
9. **Make the publishing decision** per `.agents/context/publishing.md`. Recommended:
|
||||
Gitea Actions builds `dist/` and pushes `pages`. Copy
|
||||
`.agents/templates/config/gitea-ci.yaml` to `.gitea/workflows/verify.yml`.
|
||||
10. Rewrite the publishing section of `docs/operations-guide.md` to match.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `npm run build` succeeds; `npm run gate` passes
|
||||
- [ ] `.agents/scripts/verify-hooks.sh` reports hooks live
|
||||
- [ ] A bad commit message is rejected; a raw hex in a `.astro` file is rejected
|
||||
- [ ] `/ai-for-dummies/summary/` serves correctly **from the real host**, not just preview
|
||||
- [ ] `docs/operations-guide.md` describes the actual publishing path
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not migrate more than one page. That is tasks 12–16.
|
||||
- Do not add a UI framework, CSS framework, or any runtime dependency.
|
||||
- Do not touch `hands-on/` contents, `submitted-skills/`, or `vote-service/`.
|
||||
|
||||
## Watch for
|
||||
|
||||
The base path is the #1 production-only failure in this migration. `npm run
|
||||
preview` will lie to you. Deploy the smoke-test page and curl it with a
|
||||
`?v=<sha>` cache-buster.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Task 02 — Design token layer
|
||||
|
||||
**Agent**: `design-system-keeper` · **Model**: Gemini (long context + visual judgement)
|
||||
**Depends on**: 01 · **Parallel with**: 03, 04 · **Blocks**: 07
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 02 tokens`
|
||||
|
||||
## Goal
|
||||
|
||||
One value per token, a named type scale, five breakpoints — and photographic
|
||||
proof the site looks the same.
|
||||
|
||||
## Read first
|
||||
|
||||
`.agents/context/design-system.md`. It documents two traps you will otherwise
|
||||
walk into.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/styles/tokens.css`, `src/styles/base.css`,
|
||||
`.agents/scripts/check-tokens.mjs`. You are the only writer of these.
|
||||
|
||||
## The two decisions to surface
|
||||
|
||||
1. **Three palettes → one.** `--ink` is `#172f42` / `#122534` / `#173044`;
|
||||
`--paper`, `--muted`, `--line`, `--gold` likewise. Most deltas are
|
||||
sub-perceptual — canonicalize. **`--blue` (`#527f9f` vs `#215675`) is visibly
|
||||
different**: screenshot both, get a human decision, record it here.
|
||||
2. **The fonts have never rendered.** `styles.css:1` has a malformed
|
||||
`@font-face` whose `src:` points at a Google Fonts *stylesheet*. Manrope and
|
||||
DM Mono have always fallen back to Arial and generic monospace. Default:
|
||||
delete the dead rule, declare what actually renders. Self-hosting the real
|
||||
fonts is a **redesign** and needs sign-off.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run the extraction script in `.agents/skills/design-tokens/SKILL.md`.
|
||||
2. Capture baseline screenshots: 10 routes × {560, 800, 1100, 1600} **plus the
|
||||
eight breakpoint widths you are removing** (520, 530, 600, 620, 720, 850,
|
||||
880, 900).
|
||||
3. Write `tokens.css`: one value per semantic name, `--step-*` type scale
|
||||
replacing the 14 ad-hoc `clamp()` triples, five `--bp-*` widths.
|
||||
4. Re-capture. Compare. Explain every visible difference.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Extraction prints exactly one value per token
|
||||
- [ ] `node .agents/scripts/check-tokens.mjs` passes on `src/`
|
||||
- [ ] Before/after screenshots attached at all twelve widths
|
||||
- [ ] The `--blue` decision and the font decision are written down here
|
||||
|
||||
## Do not
|
||||
|
||||
- Do not add a webfont.
|
||||
- Do not rename tokens to numeric scales (`--color-neutral-900`).
|
||||
- Do not convert the `gap:1px` over a coloured parent trick into `border` — it
|
||||
is deliberate house style and appears everywhere.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Task 03 — Verification net
|
||||
|
||||
**Agent**: `verification-engineer` · **Model**: Codex
|
||||
**Depends on**: 01 · **Parallel with**: 02, 04 · **Blocks**: 12–16
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 03 verification-net`
|
||||
|
||||
## Goal
|
||||
|
||||
Rendered-text baselines for all ten routes, captured from the **vanilla site**,
|
||||
before any page is migrated. Without this the page migrators have nothing to
|
||||
diff against and "no content was lost" becomes an opinion.
|
||||
|
||||
This task is on the critical path. Do it early.
|
||||
|
||||
## Scope
|
||||
|
||||
`.agents/snapshots/`, `scripts/audit-ui.mjs`, `.agents/scripts/visual-regression.mjs`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. `npm run serve` against the **current, unmigrated** site.
|
||||
2. Snapshot all ten routes:
|
||||
```bash
|
||||
for r in "" full-guide summary models agents skills rules skills-review \
|
||||
hands-on/starter hands-on/rules; do
|
||||
node .agents/scripts/snapshot-route.mjs "http://localhost:4173/$r/" \
|
||||
> ".agents/snapshots/${r:-index}.txt"
|
||||
done
|
||||
```
|
||||
Commit them. They are the regression net.
|
||||
3. Write `.agents/scripts/visual-regression.mjs` (Playwright). Extend the
|
||||
existing `scripts/inspect.py` pattern rather than inventing one. Baselines to
|
||||
`.agents/snapshots/before/`.
|
||||
4. **Fix the audit gap**: `audit-ui.mjs` rejects external `<script>`/`<link>`
|
||||
but misses external URLs in CSS — which is exactly how the broken Google
|
||||
Fonts `@font-face` got into this "dependency-free" site. Add `@import`,
|
||||
`src: url(https:…)`, `url(https:…)`.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Ten committed snapshots, each non-empty and containing that page's real prose
|
||||
- [ ] `visual-regression.mjs` captures 10 routes × 4 widths
|
||||
- [ ] Extended `audit-ui.mjs` **fails** on today's `styles.css` (prove it catches the real bug), then the dead rule is removed by task 02
|
||||
- [ ] `npm run gate` green
|
||||
|
||||
## Do not
|
||||
|
||||
Do not change any assertion in `verify.mjs` yet. That is task 19, after the
|
||||
pages exist.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Task 04 — Content collection schema
|
||||
|
||||
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 01 · **Parallel with**: 02, 03 · **Blocks**: 05, 06
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 04 content-schema`
|
||||
|
||||
## Goal
|
||||
|
||||
Typed collections that make a missing translation a build error.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/content/config.ts` only. You own it.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Define `const localized = z.object({ en: z.string(), pt: z.string() })`.
|
||||
**Both required.** A missing `pt` must fail the build — silent English
|
||||
fallback is how bilingual sites quietly become monolingual.
|
||||
2. Collections:
|
||||
- `guide` — `phases`, `modelGuide`, `skillSources`, `handsOnPrompts`, `skillInstallPrompts` from `app.js`
|
||||
- `chapters` — copy for `/models/`, `/agents/`, `/skills/`, `/summary/`
|
||||
- `reviews` — the 24 entries: `id`, `author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`, `improved`
|
||||
3. **Make the language-switching decision** and record it here:
|
||||
- *client-side swap* — matches today, no URL change, both languages in the payload. **Recommended.**
|
||||
- *route-based `/en/` `/pt/`* — better SEO, changes every existing URL, needs redirects.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `astro check` passes
|
||||
- [ ] A deliberately missing `pt` field fails the build (prove it, then revert)
|
||||
- [ ] The language decision is written down here with its reason
|
||||
|
||||
## Do not
|
||||
|
||||
Do not move any content yet. Schema only — tasks 05 and 06 fill it, and they
|
||||
run in parallel against the shape you define.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Task 05 — Guide content out of app.js
|
||||
|
||||
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 04 · **Parallel with**: 06 · **Blocks**: 15
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 05 content-guide`
|
||||
|
||||
## Goal
|
||||
|
||||
Every `{ en, pt }` string in `app.js` lives in `src/content/guide/`, byte-identical.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/content/guide/**`. Do **not** delete anything from `app.js` yet — task 15
|
||||
removes it once the page consumes the collection.
|
||||
|
||||
## Steps
|
||||
|
||||
1. `node .agents/scripts/extract-strings.mjs app.js > /tmp/before.json`
|
||||
(~50 pairs across `phases`, `handsOnPrompts`, `modelGuide`, `skillSources`,
|
||||
`skillInstallPrompts`).
|
||||
2. Move them into the collection. **Copy mechanically — never retype.** These
|
||||
are hand-written translations with deliberate tone (`'Transforme ambiguidade
|
||||
em trabalho'`); retyping introduces drift nobody catches until a Portuguese
|
||||
speaker reads it.
|
||||
3. `node .agents/scripts/extract-strings.mjs src/content/guide/ > /tmp/after.json`
|
||||
4. `diff /tmp/before.json /tmp/after.json` → **must be empty**.
|
||||
|
||||
## Watch for
|
||||
|
||||
`handsOnPrompts` values are multi-line prompt strings built with `.join('\n')`.
|
||||
Preserve the exact line breaks and leading `-` bullets: they are copy-pasted by
|
||||
workshop attendees into an agent, and a mangled prompt breaks the exercise.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] String diff empty
|
||||
- [ ] `astro check` passes; both locales present on every entry
|
||||
- [ ] `app.js` still untouched and the site still works
|
||||
@@ -0,0 +1,43 @@
|
||||
# Task 06 — Review-desk content out of catalog.js
|
||||
|
||||
**Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 04 · **Parallel with**: 05 · **Blocks**: 16
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 06 content-review`
|
||||
|
||||
## Goal
|
||||
|
||||
24 review entries become a typed collection, with the `improved` markdown as
|
||||
real `.md` files.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/content/reviews/**`, `scripts/build-skill-review.mjs`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Move all 24 entries from `skills-review/catalog.js` (27 KB) and
|
||||
`submitted-catalog.js` (18 KB) into the collection.
|
||||
2. `improved` becomes a real Markdown file per entry, rendered at build time.
|
||||
That deletes the hand-rolled client-side renderer.
|
||||
3. **Re-point `scripts/build-skill-review.mjs`** at the collection.
|
||||
|
||||
## The two traps
|
||||
|
||||
1. **The generator writes committed files.**
|
||||
`skill-reviews/improved/**/SKILL.md` is generated from the `improved` fields
|
||||
and the output is in git. Move `catalog.js` and the generator keeps running
|
||||
against nothing — **silently, exit code 0**. Prove it still produces
|
||||
byte-identical output:
|
||||
```bash
|
||||
node scripts/build-skill-review.mjs && git diff --exit-code skill-reviews/
|
||||
```
|
||||
2. **The diff view needs raw source.** The review desk compares original and
|
||||
improved as *text*. If `improved` only exists as rendered HTML, the change
|
||||
lens breaks. Keep the raw string reachable.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] 24 entries in the collection; `verify.mjs`'s `id:'` count assertion still passes
|
||||
- [ ] `git diff --exit-code skill-reviews/` clean after regenerating
|
||||
- [ ] `astro check` passes
|
||||
- [ ] Every reference to the generator still accurate (`package.json`, `README.md`, `docs/operations-guide.md`, the review desk footer)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Task 07 — Primitives
|
||||
|
||||
**Agent**: `component-builder` · **Model**: MiniMax-M3 — **use this task to
|
||||
calibrate the model routing before fanning out**
|
||||
**Depends on**: 02 · **Blocks**: 08–11
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 07 primitives`
|
||||
|
||||
## Goal
|
||||
|
||||
The four smallest reusable pieces, so the four parallel block tasks compose
|
||||
rather than reinvent.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/components/primitives/` — `Eyebrow.astro`, `Rule.astro`, `Callout.astro`,
|
||||
`CodeBlock.astro`. Nothing else.
|
||||
|
||||
## Why these four
|
||||
|
||||
Each appears 10+ times across the current stylesheets:
|
||||
|
||||
- **Eyebrow** — `10–11px` monospace, `letter-spacing:.08–.1em`, uppercase. The
|
||||
single most repeated treatment on the site and a signature of the design.
|
||||
- **Rule** — the `border-top: 4px solid var(--gold)` section divider.
|
||||
- **Callout** — gold-background emphasis block (`.callout`, `.thesis`).
|
||||
- **CodeBlock** — `<pre>` on `--ink` with gold text.
|
||||
|
||||
## Steps
|
||||
|
||||
Copy `.agents/templates/components/static-block.astro` for each. Typed props,
|
||||
tokens only, zero JS. Then grep the current CSS for every place each treatment
|
||||
appears and confirm the component covers them all — if it needs five variants,
|
||||
you have found two components, not one.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `.agents/checklists/before-component.md` complete for all four
|
||||
- [ ] Rendered output visually identical to the CSS classes they replace
|
||||
- [ ] `npm run gate` green
|
||||
- [ ] **Routing note written**: how many iterations, what the model got wrong.
|
||||
This decides whether 08–11 run on M3 or move to Codex.
|
||||
|
||||
## Do not
|
||||
|
||||
Do not build blocks or touch pages. Do not edit `tokens.css`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Task 08 — Route cards and grid group
|
||||
|
||||
**Agent**: `component-builder` · **Model**: MiniMax-M3
|
||||
**Depends on**: 07 · **Parallel with**: 09, 10, 11 · **Blocks**: 12
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 08 route-cards`
|
||||
|
||||
## Goal
|
||||
|
||||
The landing page's six chapter cards become one component over data.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/components/blocks/RouteCard.astro`, `src/components/blocks/GridGroup.astro`.
|
||||
|
||||
## Source
|
||||
|
||||
`index.html` — six `<article class="card">` blocks differing only in number,
|
||||
title, summary, and href. Textbook extraction. Styles in `landing.css` +
|
||||
`chapters.css`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. `GridGroup` from `.agents/templates/components/grid-group.astro`. Preserve
|
||||
the `gap:1px` over a coloured parent separator technique — it is deliberate.
|
||||
2. `RouteCard` from `static-block.astro`: `number`, `title`, `summary`, `href`.
|
||||
3. Card content moves to the `chapters` collection, both locales.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Six cards render identically to `index.html` today
|
||||
- [ ] Screenshots at 560/800/1100/1600 match
|
||||
- [ ] Zero JS
|
||||
- [ ] `.agents/checklists/before-component.md` complete; `npm run gate` green
|
||||
|
||||
## Do not
|
||||
|
||||
Do not migrate `index.html` itself — that is task 12.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Task 09 — Chapter blocks
|
||||
|
||||
**Agent**: `component-builder` · **Model**: MiniMax-M3
|
||||
**Depends on**: 07 · **Parallel with**: 08, 10, 11 · **Blocks**: 13, 14
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 09 chapter-blocks`
|
||||
|
||||
## Goal
|
||||
|
||||
The shared furniture of `/models/`, `/agents/`, `/skills/`, `/summary/`,
|
||||
`/rules/` — five pages that today duplicate the same markup five times.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/components/blocks/` — `ChapterHero.astro`, `SectionGrid.astro`,
|
||||
`ComparisonTable.astro`, `TopBar.astro`, `SiteFooter.astro`.
|
||||
`src/layouts/ChapterLayout.astro`.
|
||||
|
||||
## Source
|
||||
|
||||
`chapters.css` is already the shared layer; the duplication is in the five HTML
|
||||
files. Diff them against each other first — what differs is props, what matches
|
||||
is the component.
|
||||
|
||||
## Watch for
|
||||
|
||||
- `TopBar` carries `aria-current="page"`. Preserve it; it is the only
|
||||
indication of location for assistive tech.
|
||||
- The tables in `/models/` and `/rules/` scroll horizontally on narrow screens
|
||||
(`overflow-x:auto`, `min-width` on the inner element). Keep that — dropping it
|
||||
makes the tables unreadable on a phone, and `audit-ui.mjs` asserts related
|
||||
responsive tokens.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] All five pages' markup expressible with these components
|
||||
- [ ] Rendered text identical to current pages
|
||||
- [ ] Zero JS
|
||||
- [ ] Checklist complete; `npm run gate` green
|
||||
@@ -0,0 +1,44 @@
|
||||
# Task 10 — Full-guide blocks
|
||||
|
||||
**Agent**: `component-builder` · **Model**: MiniMax-M3
|
||||
**Depends on**: 07 · **Parallel with**: 08, 09, 11 · **Blocks**: 15
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 10 guide-blocks`
|
||||
|
||||
## Goal
|
||||
|
||||
The full guide's distinctive sections as components — **static shells only**.
|
||||
Task 15 wires the interactivity.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/components/blocks/` — `PhasePanel.astro`, `FleetDiagram.astro`,
|
||||
`HandoffTable.astro`, `WorktreeMap.astro`, `RouteTable.astro`,
|
||||
`SkillPackage.astro`.
|
||||
|
||||
## Source
|
||||
|
||||
`full-guide/index.html` (22 KB) + `styles.css` + `responsive.css`.
|
||||
|
||||
## Critical: the `data-*` hooks are asserted by name
|
||||
|
||||
`verify.mjs` requires these survive: `data-phase="plan|build|review"`,
|
||||
`data-tree="main|ui"`, `data-worker="ui"`, `data-route="plan"`,
|
||||
`data-model-provider="openai|claude|gemini"`, `data-effort="low|medium|high"`,
|
||||
`data-skill-file="skill"`, `data-skill-step="observe|validate"`,
|
||||
`data-common-skill="ponytail|caveman|unlazy"`, `role="tablist"`, `id="hands-on"`.
|
||||
|
||||
Each is a real feature hook, not decoration. Losing one is losing a feature.
|
||||
|
||||
## Watch for
|
||||
|
||||
`responsive.css` is 30 KB of append-only overrides on top of `styles.css`. **Do
|
||||
not port it wholesale.** Take what each component needs, then prove the rest is
|
||||
dead. Expect to delete most of it — that is a win, not a risk, provided the
|
||||
screenshots agree.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Every `data-*` hook above present in rendered output
|
||||
- [ ] Components take props and render static markup; **no `client:*` yet**
|
||||
- [ ] Screenshots match the current guide at four widths
|
||||
- [ ] Checklist complete; `npm run gate` green
|
||||
@@ -0,0 +1,49 @@
|
||||
# Task 11 — Review-desk blocks
|
||||
|
||||
**Agent**: `component-builder` · **Model**: MiniMax-M3
|
||||
**Depends on**: 07 · **Parallel with**: 08, 09, 10 · **Blocks**: 16
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 11 review-blocks`
|
||||
|
||||
## Goal
|
||||
|
||||
Review-desk furniture as static components. Interactivity is task 16.
|
||||
|
||||
## Scope
|
||||
|
||||
`src/components/blocks/` — `SkillList.astro`, `ReviewDetail.astro`,
|
||||
`FileTabs.astro`, `PreviewPane.astro`, `ChangeLens.astro`, `VoteWidget.astro`
|
||||
(markup only).
|
||||
|
||||
## Source
|
||||
|
||||
`skills-review/index.html`, `app.js` (20 KB), `styles.css`, `change-lens.css`,
|
||||
`vote.js`.
|
||||
|
||||
## One component, 24 entries
|
||||
|
||||
The 24 reviews are **data**, not 24 components. One `SkillReviewCard` iterating
|
||||
the collection. If you are writing the second near-identical component, stop.
|
||||
|
||||
## CSS tokens asserted by verify.mjs
|
||||
|
||||
`.change-lens`, `.change-rows`, `.skill-diff`, `.diff-lines`,
|
||||
`.markdown-preview`, `max-height:540px`, `.markdown-table-wrap`,
|
||||
`.markdown-frontmatter`, `.markdown-toc`, `.preview-title`, `.preview-markdown`,
|
||||
`grid-template-columns:minmax(0,1fr)`, `height:120px`, `-webkit-line-clamp:2`,
|
||||
`.vote-widget`, `.vote-buttons`, `[aria-pressed="true"]`.
|
||||
|
||||
Class names may move into scoped `<style>` blocks, but each must still exist and
|
||||
task 19 must be able to assert it. Coordinate names with the verification
|
||||
engineer rather than renaming freely.
|
||||
|
||||
## Watch for
|
||||
|
||||
`aria-pressed` on the vote buttons and preview switcher is how state reaches
|
||||
assistive tech. Colour alone is not enough. It is also asserted.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] All listed CSS hooks present
|
||||
- [ ] `aria-pressed`, `role="group"`, `aria-label` preserved
|
||||
- [ ] Static render matches current desk at four widths
|
||||
- [ ] Checklist complete; `npm run gate` green
|
||||
@@ -0,0 +1,30 @@
|
||||
# Task 12 — Landing page
|
||||
|
||||
**Agent**: `page-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 03, 08 · **Parallel with**: 13, 14, 17
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 12 page-landing`
|
||||
|
||||
## Goal
|
||||
|
||||
`index.html` → `src/pages/index.astro`. Smallest page migration; sets the
|
||||
pattern the other page tasks follow.
|
||||
|
||||
## Steps
|
||||
|
||||
Snapshot first (task 03 baseline exists — diff against it). Then compose from
|
||||
`RouteCard` + `GridGroup`, move copy to the `chapters` collection, port
|
||||
`landing.css` into component styles.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- URL stays `/ai-for-dummies/` exactly
|
||||
- **Zero JS** — this page has none today
|
||||
- `<title>`, `<meta name="description">`, viewport preserved
|
||||
- Internal links via `import.meta.env.BASE_URL`, never hand-written absolute
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `diff .agents/snapshots/index.txt <(snapshot of dist)` empty
|
||||
- [ ] Screenshots match at four widths
|
||||
- [ ] Built page ships **0 bytes** of JS
|
||||
- [ ] `.agents/checklists/before-page.md` complete; `npm run gate` green
|
||||
@@ -0,0 +1,33 @@
|
||||
# Task 13 — Chapter pages ×4
|
||||
|
||||
**Agent**: `page-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 03, 09 · **Parallel with**: 12, 14, 17 · **Blocks**: 15, 16
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 13 page-chapters`
|
||||
|
||||
## Goal
|
||||
|
||||
`/models/`, `/agents/`, `/skills/`, `/summary/` as Astro routes. One task
|
||||
because they share a layout — four near-identical migrations, and doing them
|
||||
together is what proves `ChapterLayout` is right.
|
||||
|
||||
Blocks 15 and 16 because those pages link here and share the layout.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- Four URLs unchanged, **trailing slashes included**
|
||||
- `/skills/` has an interactive package explorer (`skills/app.js`,
|
||||
`data-skill-file`, `data-skill-step`) — that becomes an **island**, and it is
|
||||
the only JS across these four pages. The other three ship zero.
|
||||
- All `data-*` hooks preserved; `verify.mjs` asserts several
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Four snapshot diffs empty
|
||||
- [ ] Three pages ship 0 bytes JS; `/skills/` ships only its island
|
||||
- [ ] Screenshots match at four widths
|
||||
- [ ] Checklist complete for each page; `npm run gate` green
|
||||
|
||||
## Do not
|
||||
|
||||
Do not generalise `ChapterLayout` beyond these four. `/rules/` is task 14 and
|
||||
has its own interaction model.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Task 14 — Rules page
|
||||
|
||||
**Agent**: `page-migrator` · **Model**: MiniMax-M3
|
||||
**Depends on**: 03, 09 · **Parallel with**: 12, 13, 17
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 14 page-rules`
|
||||
|
||||
## Goal
|
||||
|
||||
`/rules/` → Astro. Separate from task 13: it is a standalone bilingual case
|
||||
study with its own stylesheet (`rules/styles.css`, 9.8 KB), its own script
|
||||
(`rules/app.js`, 16 KB), and its own responsive contract.
|
||||
|
||||
## Asserted by verify.mjs
|
||||
|
||||
- `rulesHtml` must have **no external `<script>`/`<link>`** — it is explicitly
|
||||
checked as a standalone page
|
||||
- `rulesCss` must contain `@media(min-width:2200px)`, `@media(max-width:900px)`,
|
||||
`@media(max-width:600px)`, `prefers-reduced-motion`
|
||||
|
||||
Task 02 collapses those breakpoints to named tokens. **Coordinate with the
|
||||
verification engineer** — the assertion must be re-pointed at the token, not
|
||||
deleted. Screenshot at the old widths (900, 600, 2200) to prove equivalence.
|
||||
|
||||
## Watch for
|
||||
|
||||
`rules/styles.css` shares `--accent` and `--deep` with `styles.css` — it is on
|
||||
the *first* palette. Confirm task 02 canonicalized it the same way.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Snapshot diff empty; bilingual toggle works, `<html lang>` follows
|
||||
- [ ] Responsive behaviour identical at 600/900/2200
|
||||
- [ ] No external dependency; checklist complete; `npm run gate` green
|
||||
@@ -0,0 +1,62 @@
|
||||
# Task 15 — Full guide
|
||||
|
||||
**Agent**: `page-migrator` · **Model**: **Codex** — hardest task in the plan
|
||||
**Depends on**: 05, 10, 13 · **Parallel with**: 16
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 15 page-full-guide`
|
||||
|
||||
## Goal
|
||||
|
||||
`/full-guide/` → Astro. 22 KB of HTML, a 50 KB script, 30 KB of CSS, twelve
|
||||
render functions, bilingual throughout.
|
||||
|
||||
## What `app.js` actually is
|
||||
|
||||
Not application code — a **bilingual content database** (task 05 already moved
|
||||
it) plus ~12 `render*` functions that swap `innerHTML` on tab clicks. Once
|
||||
content is a collection, the remaining JS is small: tab state and a language
|
||||
toggle.
|
||||
|
||||
## Islands
|
||||
|
||||
Only these hydrate. Everything else is server-rendered.
|
||||
|
||||
| Island | Directive | Why |
|
||||
| --- | --- | --- |
|
||||
| Phase tabs | `client:visible` | click-driven panel swap |
|
||||
| Tree / worker / route / model / effort / skill selectors | `client:visible` | same pattern; consider one generic selector island |
|
||||
| Language toggle | `client:idle` | page-wide, not urgent |
|
||||
| Copy-prompt buttons | `client:visible` | clipboard |
|
||||
|
||||
If you end up with 12 separate islands you have missed the pattern — they are
|
||||
one selector component with different data.
|
||||
|
||||
## Asserted by verify.mjs — all must survive
|
||||
|
||||
`const phases`, `const handsOnPrompts`, `const modelGuide`, `const skillSources`,
|
||||
`const skillInstallPrompts`, `render('plan')`, `renderTree`, `renderWorker`,
|
||||
`renderRoute`, `renderModelProvider`, `renderEffort`, `renderSkillFile`,
|
||||
`renderSkillWorkflow`, `renderCommonSkill`, `renderHandsOn`, `copyPrompt`.
|
||||
|
||||
These are **implementation-detail assertions** — they look deletable and are
|
||||
not. Each pins a feature. Coordinate with task 19 to replace each with an
|
||||
output-level assertion of the same behaviour. **Never delete one yourself.**
|
||||
|
||||
Also: `data-copy-target="prompt-install-skills|prompt-basic|prompt-skills"`,
|
||||
`hands-on/starter/`, `additional-reading.md`, `role="tablist"`, `<table>`.
|
||||
|
||||
## Watch for
|
||||
|
||||
- `copyPrompt` uses `navigator.clipboard` with a `document.execCommand`
|
||||
fallback. Keep both — the fallback exists for non-secure contexts.
|
||||
- The hands-on prompt strings are copy-pasted by attendees into an agent.
|
||||
Exact whitespace and line breaks matter.
|
||||
- `responsive.css` (30 KB) mostly serves this page. Port what is needed, prove
|
||||
the rest dead, delete it. Screenshots are the proof.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Snapshot diff empty
|
||||
- [ ] Every interaction works: all tabs, both languages, all copy buttons
|
||||
- [ ] Keyboard: arrow keys move between tabs; focus visible throughout
|
||||
- [ ] JS payload **smaller** than today's 50 KB (content is now static)
|
||||
- [ ] Screenshots match at four widths; checklist complete; `npm run gate` green
|
||||
@@ -0,0 +1,63 @@
|
||||
# Task 16 — Review desk
|
||||
|
||||
**Agent**: `page-migrator` · **Model**: **Codex** — highest defect risk
|
||||
**Depends on**: 06, 11, 13 · **Parallel with**: 15
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 16 page-review-desk`
|
||||
|
||||
## Goal
|
||||
|
||||
`/skills-review/` → Astro. The most interactive page: search, filtering,
|
||||
lazy file fetching, markdown rendering, a client-side diff view, six URL params,
|
||||
and a live API call to `vote-service/`.
|
||||
|
||||
## The six query params are a public contract
|
||||
|
||||
`?author=`, `?skill=`, `?view=`, `?file=`, `?compare=`, `?render=` — documented
|
||||
in the page footer and shared externally. All six must round-trip, and browser
|
||||
back/forward must restore state (`syncUrl` / `selectFromUrl` today).
|
||||
|
||||
**Test every one manually.** A snapshot diff cannot catch a broken deep link.
|
||||
|
||||
## Islands
|
||||
|
||||
The catalog + detail pane is genuinely interactive: `client:load` is justified
|
||||
here. The vote widget is `client:visible`. Everything else server-renders.
|
||||
|
||||
## vote-service integration
|
||||
|
||||
`window.SKILLS_REVIEW_VOTE_API = 'https://ai-for-dummies-vote.marcospaulo.dev.br'`
|
||||
is set inline in `index.html` today. Either keep the global or move it to
|
||||
`PUBLIC_VOTE_API` — **if you move it, update `vote-service/README.md` in the
|
||||
same change**, since it documents the coupling.
|
||||
|
||||
`ALLOWED_ORIGIN` on the deployed service is
|
||||
`https://netcracker.pages.marcospaulo.dev.br`. If the site's origin changes, the
|
||||
vote API breaks with a CORS error. It does not change in this plan — but verify
|
||||
after cutover.
|
||||
|
||||
## Asserted by verify.mjs
|
||||
|
||||
Interaction tokens: `from './catalog.js'`, `from './files.js'`, `renderList`,
|
||||
`renderDetail`, `selectSkill`, `packageSummary`, `markdownHeadings`,
|
||||
`markdownToc`, `loadSelectedFile`, `schedulePackageSearch`, `fetchSource`,
|
||||
`packageSearchText`, `diffMarkup`, `diffRows`, `searchParams.set('compare')`,
|
||||
`markdownMarkup`, `syncUrl`, `selectFromUrl`, `URLSearchParams`,
|
||||
`navigator.clipboard`, `document.execCommand`.
|
||||
|
||||
Plus the catalog count: `id:'` occurrences across both catalogs **must equal 24**.
|
||||
|
||||
Same rule as task 15 — re-point with task 19, never delete.
|
||||
|
||||
## Watch for
|
||||
|
||||
The markdown renderer is hand-rolled (`markdownMarkup`, `markdownHeadings`,
|
||||
`markdownToc`). Task 06 moves rendering to build time — but the **diff view
|
||||
needs raw source text**, not rendered HTML. Keep both available.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] All six query params round-trip; back/forward restores state
|
||||
- [ ] Search, filter, file tabs, preview, change lens, download, copy all work
|
||||
- [ ] Vote widget reaches the live API; CORS preflight succeeds
|
||||
- [ ] Snapshot diff empty; screenshots match; checklist complete
|
||||
- [ ] `npm run gate` green
|
||||
@@ -0,0 +1,38 @@
|
||||
# Task 17 — hands-on passthrough
|
||||
|
||||
**Agent**: `astro-architect` · **Model**: MiniMax-M3
|
||||
**Depends on**: 01 · **Parallel with**: 12, 13, 14
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 17 hands-on`
|
||||
|
||||
## Goal
|
||||
|
||||
`hands-on/starter/` and `hands-on/rules/` ship **byte-identical** from
|
||||
`public/`, unprocessed.
|
||||
|
||||
## Why this is a task and not a footnote
|
||||
|
||||
These are **lab fixtures**. The workshop exercise is that an attendee points an
|
||||
agent at dependency-free HTML/CSS/JS and watches it work. Componentizing them,
|
||||
minifying them, or letting a bundler touch them destroys the lesson — and the
|
||||
prompts in `app.js` reference these files by path and content.
|
||||
|
||||
The risk is an agent "helpfully" improving them. This task exists to say
|
||||
explicitly: do not.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Move both directories to `public/hands-on/`.
|
||||
2. Confirm Astro copies `public/` verbatim with no processing.
|
||||
3. Confirm URLs `/ai-for-dummies/hands-on/starter/` and `.../rules/` resolve.
|
||||
4. Confirm every lint/format config **ignores** them (the templates already do).
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `diff -r` between the original directories and `dist/hands-on/` is empty
|
||||
- [ ] Both URLs serve; both labs work standalone
|
||||
- [ ] The `hands-on/starter/` reference in the full guide still resolves
|
||||
- [ ] `verify.mjs`'s `starterHtml` / `starterJs` assertions still pass
|
||||
|
||||
## Do not
|
||||
|
||||
Do not reformat, lint, componentize, or "modernise" a single line.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Task 18 — Motion pass
|
||||
|
||||
**Agent**: `motion-designer` · **Model**: Gemini (perceptual judgement)
|
||||
**Depends on**: 15, 16 · **Parallel with**: 19
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 18 motion`
|
||||
|
||||
## Goal
|
||||
|
||||
Audit the motion that exists, add only motion that earns its place, and make
|
||||
`prefers-reduced-motion` correct everywhere.
|
||||
|
||||
## Order of work — audit first
|
||||
|
||||
The audit is worth more than the additions.
|
||||
|
||||
1. Inventory every `transition`, `animation`, `@keyframes`, `transform` in `src/`.
|
||||
2. Flag anything animating a layout property (`width`, `height`, `top`, `left`,
|
||||
`margin`). Those force reflow every frame and fail the 200ms INP budget. The
|
||||
Stylelint config already blocks new ones; find the ported ones.
|
||||
3. Confirm every animated element has a reduced-motion path, then **test it**
|
||||
under DevTools emulation. Reduced means reduced, not broken — end states must
|
||||
still be correct.
|
||||
|
||||
## Then, sparingly
|
||||
|
||||
Candidates, each needing a written purpose:
|
||||
|
||||
- tab panel change in the guide (state change — justified)
|
||||
- review-desk detail swap on selection (state change — justified)
|
||||
- vote widget tally update (feedback — justified)
|
||||
|
||||
`transform` and `opacity` only. 150–250ms. `cubic-bezier(.2,0,0,1)`. One thing
|
||||
moves at a time — no staggered card cascades; this design has a point of view
|
||||
and staggering reads as a template.
|
||||
|
||||
## View transitions — optional, decide explicitly
|
||||
|
||||
Astro's `<ClientRouter />` is the only sanctioned motion dependency. If adopted,
|
||||
verify: JS-disabled navigation, browser back/forward, the review desk's six
|
||||
query params, and reduced motion. If any fails, do not ship it.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Zero animations on layout properties
|
||||
- [ ] Every animation has a one-line written purpose
|
||||
- [ ] Reduced-motion tested under emulation; end states correct
|
||||
- [ ] Before/after captures attached; `npm run gate` green
|
||||
@@ -0,0 +1,44 @@
|
||||
# Task 19 — Re-point the verification contract
|
||||
|
||||
**Agent**: `verification-engineer` · **Model**: Codex
|
||||
**Depends on**: 15, 16 · **Parallel with**: 18
|
||||
**Worktree**: `.agents/scripts/worktree.sh start 19 verify-repoint`
|
||||
|
||||
## Goal
|
||||
|
||||
All 42 assertions pin the same user-visible facts against the new architecture.
|
||||
Coverage does not fall.
|
||||
|
||||
You are the only role permitted to remove an assertion, and every removal needs
|
||||
a written reason.
|
||||
|
||||
## The three kinds
|
||||
|
||||
| Kind | Example | What to do |
|
||||
| --- | --- | --- |
|
||||
| Content presence | `data-phase="plan"` | re-point at `dist/full-guide/index.html`; the token should survive rendering. If it does not, a component dropped content — **stop and report** |
|
||||
| Implementation detail | `const phases`, `renderTree`, `from './catalog.js'` | obsolete as written, but each pins a **feature**. Replace with an output-level assertion of that feature. Never drop |
|
||||
| Asset version | `app.js?v=20260904-vote-widget` | Astro hashes assets — assert the built HTML references a hashed asset |
|
||||
|
||||
## Steps
|
||||
|
||||
1. `npm run build`, then re-point `read()` calls at `dist/`.
|
||||
2. Work through all 42 in order. For each: does the fact it pins still exist?
|
||||
Yes → re-point. No → content was lost; escalate.
|
||||
3. Add rendered-text snapshot assertions for all ten routes so this class of
|
||||
regression is caught structurally, not by string luck.
|
||||
4. Confirm `check-tokens.mjs` and the extended `audit-ui.mjs` are in
|
||||
`npm run verify`.
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] `grep -c 'throw new Error' scripts/verify.mjs` ≥ the `origin/main` baseline
|
||||
- [ ] Every removal has a one-line reason in this file
|
||||
- [ ] Snapshot assertions cover all ten routes
|
||||
- [ ] `npm run gate` green, and it **fails** when you deliberately delete a
|
||||
paragraph from a component (prove the net works, then revert)
|
||||
|
||||
## Do not
|
||||
|
||||
Do not weaken an assertion to make it pass. If it cannot pass, something is
|
||||
broken — that is the assertion doing its job.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Task 20 — Cutover and cleanup
|
||||
|
||||
**Agent**: `astro-architect`, **with a human watching** · **Model**: MiniMax-M3 assisting
|
||||
**Depends on**: all · **Worktree**: `.agents/scripts/worktree.sh start 20 cutover`
|
||||
|
||||
This task touches production publishing. Do not run it unattended.
|
||||
|
||||
## Goal
|
||||
|
||||
The Astro build is what the world sees, the old files are gone, and the docs
|
||||
describe reality.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Full verification** on the built site: all 10 routes, all 6 query params,
|
||||
both languages, vote widget against the live API, screenshots at four widths.
|
||||
2. **Delete the superseded files** — only after their replacements are proven:
|
||||
`app.js`, `styles.css`, `responsive.css`, `chapters.css`, `landing.css`,
|
||||
`rules/app.js`, `rules/styles.css`, `skills/app.js`, `skills/styles.css`,
|
||||
`skills-review/*.js`, `skills-review/*.css`, and the ten old `index.html`
|
||||
files. `git rm`, one commit, reviewable.
|
||||
**Keep**: `hands-on/**` (now under `public/`), `submitted-skills/**`,
|
||||
`skill-reviews/**`, `docs/**`, `vote-service/**`.
|
||||
3. **Publish** via the mechanism chosen in task 01.
|
||||
4. **Verify on the real host** with a cache-buster:
|
||||
```bash
|
||||
for r in "" full-guide summary models agents skills rules skills-review \
|
||||
hands-on/starter hands-on/rules; do
|
||||
curl -sS -o /dev/null -w "%{http_code} $r\n" \
|
||||
"https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/$r/?v=$(git rev-parse --short HEAD)"
|
||||
done
|
||||
```
|
||||
All ten must be 200. A stale cached 200 looks identical to success — the
|
||||
`?v=` is what distinguishes them.
|
||||
5. **Update the docs**: `README.md`, `docs/operations-guide.md` (build + publish
|
||||
path, and it must stop claiming `pages` is "the exact published source" if
|
||||
that is no longer true), `GATES.md`, and `AGENTS.md` (stack is no longer
|
||||
"migration in progress").
|
||||
6. **Fast-forward `pages`** per the procedure in `docs/operations-guide.md`.
|
||||
|
||||
## Rollback
|
||||
|
||||
`pages` still holds the working vanilla site until you overwrite it. If cutover
|
||||
fails, reset `pages` to its previous commit — the old site returns immediately.
|
||||
Note the SHA before you start:
|
||||
|
||||
```bash
|
||||
git rev-parse origin/pages # write it down
|
||||
```
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Ten routes 200 on the real host with a fresh cache-buster
|
||||
- [ ] Vote widget works end-to-end from the published origin (CORS is
|
||||
origin-sensitive — `ALLOWED_ORIGIN` must still match)
|
||||
- [ ] Old files deleted; `npm run gate` green
|
||||
- [ ] Docs match reality
|
||||
- [ ] Previous `pages` SHA recorded for rollback
|
||||
Reference in New Issue
Block a user