From aae4d422299344766699d596354391ee3f2d8537 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Date: Sat, 5 Sep 2026 01:18:27 +0000 Subject: [PATCH] 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 --- .agents/ORCHESTRATOR.md | 64 +++++++++++ .agents/agents/astro-architect.md | 44 +++++++ .agents/agents/component-builder.md | 37 ++++++ .agents/agents/content-i18n-migrator.md | 53 +++++++++ .agents/agents/design-system-keeper.md | 46 ++++++++ .agents/agents/motion-designer.md | 48 ++++++++ .agents/agents/page-migrator.md | 42 +++++++ .agents/agents/reviewer.md | 50 ++++++++ .agents/agents/verification-engineer.md | 50 ++++++++ .agents/checklists/before-component.md | 15 +++ .agents/checklists/before-merge.md | 12 ++ .agents/checklists/before-page.md | 14 +++ .agents/context/architecture.md | 75 ++++++++++++ .agents/context/design-system.md | 107 ++++++++++++++++++ .agents/context/publishing.md | 54 +++++++++ .agents/context/verification.md | 54 +++++++++ .agents/rules/accessibility.md | 52 +++++++++ .agents/rules/animation.md | 59 ++++++++++ .agents/rules/astro.md | 72 ++++++++++++ .agents/rules/code-style.md | 48 ++++++++ .agents/rules/componentization.md | 53 +++++++++ .agents/rules/content-i18n.md | 59 ++++++++++ .agents/rules/gates.md | 76 +++++++++++++ .agents/rules/git-worktrees.md | 69 +++++++++++ .agents/rules/theming.md | 78 +++++++++++++ .agents/scripts/check-tokens.mjs | 59 ++++++++++ .agents/scripts/extract-strings.mjs | 45 ++++++++ .agents/scripts/gate.sh | 61 ++++++++++ .agents/scripts/install-claude-agents.sh | 29 +++++ .agents/scripts/snapshot-route.mjs | 37 ++++++ .agents/scripts/verify-hooks.sh | 51 +++++++++ .agents/scripts/worktree.sh | 50 ++++++++ .agents/skills/astro-component/SKILL.md | 68 +++++++++++ .agents/skills/astro-page/SKILL.md | 65 +++++++++++ .agents/skills/content-migration/SKILL.md | 73 ++++++++++++ .agents/skills/design-tokens/SKILL.md | 70 ++++++++++++ .agents/skills/motion/SKILL.md | 68 +++++++++++ .agents/skills/verify-contract/SKILL.md | 64 +++++++++++ .agents/skills/visual-regression/SKILL.md | 71 ++++++++++++ .agents/templates/components/grid-group.astro | 39 +++++++ .agents/templates/components/island.astro | 97 ++++++++++++++++ .../templates/components/static-block.astro | 74 ++++++++++++ .agents/templates/config/.prettierignore | 7 ++ .agents/templates/config/.prettierrc.json | 11 ++ .agents/templates/config/.stylelintrc.json | 22 ++++ .agents/templates/config/eslint.config.js | 26 +++++ .agents/templates/config/gitea-ci.yaml | 39 +++++++ .agents/templates/config/package.scripts.json | 27 +++++ .agents/templates/pages/chapter.astro | 61 ++++++++++ .agents/templates/pages/interactive.astro | 45 ++++++++ .gitignore | 11 ++ .husky/commit-msg | 26 +++++ .husky/pre-commit | 10 ++ .husky/pre-push | 4 + .lintstagedrc.json | 16 +++ AGENTS.md | 77 +++++++++++++ CLAUDE.md | 1 + plans/astro-refactor/MODEL-ROUTING.md | 73 ++++++++++++ plans/astro-refactor/README.md | 94 +++++++++++++++ plans/astro-refactor/task-01-scaffold.md | 59 ++++++++++ plans/astro-refactor/task-02-tokens.md | 56 +++++++++ .../task-03-verification-net.md | 49 ++++++++ .../astro-refactor/task-04-content-schema.md | 37 ++++++ plans/astro-refactor/task-05-content-guide.md | 38 +++++++ .../astro-refactor/task-06-content-review.md | 43 +++++++ plans/astro-refactor/task-07-primitives.md | 45 ++++++++ plans/astro-refactor/task-08-route-cards.md | 37 ++++++ .../astro-refactor/task-09-chapter-blocks.md | 38 +++++++ plans/astro-refactor/task-10-guide-blocks.md | 44 +++++++ plans/astro-refactor/task-11-review-blocks.md | 49 ++++++++ plans/astro-refactor/task-12-page-landing.md | 30 +++++ plans/astro-refactor/task-13-page-chapters.md | 33 ++++++ plans/astro-refactor/task-14-page-rules.md | 33 ++++++ .../astro-refactor/task-15-page-full-guide.md | 62 ++++++++++ .../task-16-page-review-desk.md | 63 +++++++++++ plans/astro-refactor/task-17-hands-on.md | 38 +++++++ plans/astro-refactor/task-18-motion.md | 47 ++++++++ .../astro-refactor/task-19-verify-repoint.md | 44 +++++++ plans/astro-refactor/task-20-cutover.md | 58 ++++++++++ 79 files changed, 3805 insertions(+) create mode 100644 .agents/ORCHESTRATOR.md create mode 100644 .agents/agents/astro-architect.md create mode 100644 .agents/agents/component-builder.md create mode 100644 .agents/agents/content-i18n-migrator.md create mode 100644 .agents/agents/design-system-keeper.md create mode 100644 .agents/agents/motion-designer.md create mode 100644 .agents/agents/page-migrator.md create mode 100644 .agents/agents/reviewer.md create mode 100644 .agents/agents/verification-engineer.md create mode 100644 .agents/checklists/before-component.md create mode 100644 .agents/checklists/before-merge.md create mode 100644 .agents/checklists/before-page.md create mode 100644 .agents/context/architecture.md create mode 100644 .agents/context/design-system.md create mode 100644 .agents/context/publishing.md create mode 100644 .agents/context/verification.md create mode 100644 .agents/rules/accessibility.md create mode 100644 .agents/rules/animation.md create mode 100644 .agents/rules/astro.md create mode 100644 .agents/rules/code-style.md create mode 100644 .agents/rules/componentization.md create mode 100644 .agents/rules/content-i18n.md create mode 100644 .agents/rules/gates.md create mode 100644 .agents/rules/git-worktrees.md create mode 100644 .agents/rules/theming.md create mode 100755 .agents/scripts/check-tokens.mjs create mode 100755 .agents/scripts/extract-strings.mjs create mode 100755 .agents/scripts/gate.sh create mode 100755 .agents/scripts/install-claude-agents.sh create mode 100755 .agents/scripts/snapshot-route.mjs create mode 100755 .agents/scripts/verify-hooks.sh create mode 100755 .agents/scripts/worktree.sh create mode 100644 .agents/skills/astro-component/SKILL.md create mode 100644 .agents/skills/astro-page/SKILL.md create mode 100644 .agents/skills/content-migration/SKILL.md create mode 100644 .agents/skills/design-tokens/SKILL.md create mode 100644 .agents/skills/motion/SKILL.md create mode 100644 .agents/skills/verify-contract/SKILL.md create mode 100644 .agents/skills/visual-regression/SKILL.md create mode 100644 .agents/templates/components/grid-group.astro create mode 100644 .agents/templates/components/island.astro create mode 100644 .agents/templates/components/static-block.astro create mode 100644 .agents/templates/config/.prettierignore create mode 100644 .agents/templates/config/.prettierrc.json create mode 100644 .agents/templates/config/.stylelintrc.json create mode 100644 .agents/templates/config/eslint.config.js create mode 100644 .agents/templates/config/gitea-ci.yaml create mode 100644 .agents/templates/config/package.scripts.json create mode 100644 .agents/templates/pages/chapter.astro create mode 100644 .agents/templates/pages/interactive.astro create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 .husky/pre-push create mode 100644 .lintstagedrc.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 plans/astro-refactor/MODEL-ROUTING.md create mode 100644 plans/astro-refactor/README.md create mode 100644 plans/astro-refactor/task-01-scaffold.md create mode 100644 plans/astro-refactor/task-02-tokens.md create mode 100644 plans/astro-refactor/task-03-verification-net.md create mode 100644 plans/astro-refactor/task-04-content-schema.md create mode 100644 plans/astro-refactor/task-05-content-guide.md create mode 100644 plans/astro-refactor/task-06-content-review.md create mode 100644 plans/astro-refactor/task-07-primitives.md create mode 100644 plans/astro-refactor/task-08-route-cards.md create mode 100644 plans/astro-refactor/task-09-chapter-blocks.md create mode 100644 plans/astro-refactor/task-10-guide-blocks.md create mode 100644 plans/astro-refactor/task-11-review-blocks.md create mode 100644 plans/astro-refactor/task-12-page-landing.md create mode 100644 plans/astro-refactor/task-13-page-chapters.md create mode 100644 plans/astro-refactor/task-14-page-rules.md create mode 100644 plans/astro-refactor/task-15-page-full-guide.md create mode 100644 plans/astro-refactor/task-16-page-review-desk.md create mode 100644 plans/astro-refactor/task-17-hands-on.md create mode 100644 plans/astro-refactor/task-18-motion.md create mode 100644 plans/astro-refactor/task-19-verify-repoint.md create mode 100644 plans/astro-refactor/task-20-cutover.md diff --git a/.agents/ORCHESTRATOR.md b/.agents/ORCHESTRATOR.md new file mode 100644 index 0000000..5bf9f62 --- /dev/null +++ b/.agents/ORCHESTRATOR.md @@ -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/.md + its skills + ↓ +checklists/before-*.md ← self-gate + ↓ +npm run verify ← hard gate + ↓ +reviewer agent on the diff ← merge gate +``` diff --git a/.agents/agents/astro-architect.md b/.agents/agents/astro-architect.md new file mode 100644 index 0000000..d6a2c08 --- /dev/null +++ b/.agents/agents/astro-architect.md @@ -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. diff --git a/.agents/agents/component-builder.md b/.agents/agents/component-builder.md new file mode 100644 index 0000000..6bdc1ee --- /dev/null +++ b/.agents/agents/component-builder.md @@ -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**. diff --git a/.agents/agents/content-i18n-migrator.md b/.agents/agents/content-i18n-migrator.md new file mode 100644 index 0000000..6084d02 --- /dev/null +++ b/.agents/agents/content-i18n-migrator.md @@ -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 `` 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. diff --git a/.agents/agents/design-system-keeper.md b/.agents/agents/design-system-keeper.md new file mode 100644 index 0000000..6b734f3 --- /dev/null +++ b/.agents/agents/design-system-keeper.md @@ -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. diff --git a/.agents/agents/motion-designer.md b/.agents/agents/motion-designer.md new file mode 100644 index 0000000..14a13ee --- /dev/null +++ b/.agents/agents/motion-designer.md @@ -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 `` 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. diff --git a/.agents/agents/page-migrator.md b/.agents/agents/page-migrator.md new file mode 100644 index 0000000..0c11d4d --- /dev/null +++ b/.agents/agents/page-migrator.md @@ -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, ``, 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. diff --git a/.agents/agents/reviewer.md b/.agents/agents/reviewer.md new file mode 100644 index 0000000..268b1c6 --- /dev/null +++ b/.agents/agents/reviewer.md @@ -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. diff --git a/.agents/agents/verification-engineer.md b/.agents/agents/verification-engineer.md new file mode 100644 index 0000000..4209122 --- /dev/null +++ b/.agents/agents/verification-engineer.md @@ -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. diff --git a/.agents/checklists/before-component.md b/.agents/checklists/before-component.md new file mode 100644 index 0000000..207f373 --- /dev/null +++ b/.agents/checklists/before-component.md @@ -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 diff --git a/.agents/checklists/before-merge.md b/.agents/checklists/before-merge.md new file mode 100644 index 0000000..2bd52fd --- /dev/null +++ b/.agents/checklists/before-merge.md @@ -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 diff --git a/.agents/checklists/before-page.md b/.agents/checklists/before-page.md new file mode 100644 index 0000000..e127bc2 --- /dev/null +++ b/.agents/checklists/before-page.md @@ -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 diff --git a/.agents/context/architecture.md b/.agents/context/architecture.md new file mode 100644 index 0000000..1220b53 --- /dev/null +++ b/.agents/context/architecture.md @@ -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. diff --git a/.agents/context/design-system.md b/.agents/context/design-system.md new file mode 100644 index 0000000..e7f770f --- /dev/null +++ b/.agents/context/design-system.md @@ -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`. diff --git a/.agents/context/publishing.md b/.agents/context/publishing.md new file mode 100644 index 0000000..44d83ad --- /dev/null +++ b/.agents/context/publishing.md @@ -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. diff --git a/.agents/context/verification.md b/.agents/context/verification.md new file mode 100644 index 0000000..9c8037c --- /dev/null +++ b/.agents/context/verification.md @@ -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. diff --git a/.agents/rules/accessibility.md b/.agents/rules/accessibility.md new file mode 100644 index 0000000..beaf75f --- /dev/null +++ b/.agents/rules/accessibility.md @@ -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. diff --git a/.agents/rules/animation.md b/.agents/rules/animation.md new file mode 100644 index 0000000..6ecdffc --- /dev/null +++ b/.agents/rules/animation.md @@ -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. diff --git a/.agents/rules/astro.md b/.agents/rules/astro.md new file mode 100644 index 0000000..5bad9b5 --- /dev/null +++ b/.agents/rules/astro.md @@ -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. diff --git a/.agents/rules/code-style.md b/.agents/rules/code-style.md new file mode 100644 index 0000000..7d93492 --- /dev/null +++ b/.agents/rules/code-style.md @@ -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. diff --git a/.agents/rules/componentization.md b/.agents/rules/componentization.md new file mode 100644 index 0000000..4adb3fc --- /dev/null +++ b/.agents/rules/componentization.md @@ -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. diff --git a/.agents/rules/content-i18n.md b/.agents/rules/content-i18n.md new file mode 100644 index 0000000..0e9c724 --- /dev/null +++ b/.agents/rules/content-i18n.md @@ -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. diff --git a/.agents/rules/gates.md b/.agents/rules/gates.md new file mode 100644 index 0000000..d20fcd1 --- /dev/null +++ b/.agents/rules/gates.md @@ -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. diff --git a/.agents/rules/git-worktrees.md b/.agents/rules/git-worktrees.md new file mode 100644 index 0000000..4e0b9fa --- /dev/null +++ b/.agents/rules/git-worktrees.md @@ -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. diff --git a/.agents/rules/theming.md b/.agents/rules/theming.md new file mode 100644 index 0000000..4f40fac --- /dev/null +++ b/.agents/rules/theming.md @@ -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. diff --git a/.agents/scripts/check-tokens.mjs b/.agents/scripts/check-tokens.mjs new file mode 100755 index 0000000..9363dff --- /dev/null +++ b/.agents/scripts/check-tokens.mjs @@ -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'); diff --git a/.agents/scripts/extract-strings.mjs b/.agents/scripts/extract-strings.mjs new file mode 100755 index 0000000..b7fdd18 --- /dev/null +++ b/.agents/scripts/extract-strings.mjs @@ -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)`); diff --git a/.agents/scripts/gate.sh b/.agents/scripts/gate.sh new file mode 100755 index 0000000..4160b37 --- /dev/null +++ b/.agents/scripts/gate.sh @@ -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 ))" diff --git a/.agents/scripts/install-claude-agents.sh b/.agents/scripts/install-claude-agents.sh new file mode 100755 index 0000000..395dd78 --- /dev/null +++ b/.agents/scripts/install-claude-agents.sh @@ -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|/$||' diff --git a/.agents/scripts/snapshot-route.mjs b/.agents/scripts/snapshot-route.mjs new file mode 100755 index 0000000..5ff5a5f --- /dev/null +++ b/.agents/scripts/snapshot-route.mjs @@ -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); diff --git a/.agents/scripts/verify-hooks.sh b/.agents/scripts/verify-hooks.sh new file mode 100755 index 0000000..d52fd5e --- /dev/null +++ b/.agents/scripts/verify-hooks.sh @@ -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" diff --git a/.agents/scripts/worktree.sh b/.agents/scripts/worktree.sh new file mode 100755 index 0000000..363459a --- /dev/null +++ b/.agents/scripts/worktree.sh @@ -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 diff --git a/.agents/skills/astro-component/SKILL.md b/.agents/skills/astro-component/SKILL.md new file mode 100644 index 0000000..7631a8f --- /dev/null +++ b/.agents/skills/astro-component/SKILL.md @@ -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. diff --git a/.agents/skills/astro-page/SKILL.md b/.agents/skills/astro-page/SKILL.md new file mode 100644 index 0000000..fecce4a --- /dev/null +++ b/.agents/skills/astro-page/SKILL.md @@ -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. diff --git a/.agents/skills/content-migration/SKILL.md b/.agents/skills/content-migration/SKILL.md new file mode 100644 index 0000000..c9b27a8 --- /dev/null +++ b/.agents/skills/content-migration/SKILL.md @@ -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. diff --git a/.agents/skills/design-tokens/SKILL.md b/.agents/skills/design-tokens/SKILL.md new file mode 100644 index 0000000..85ae22e --- /dev/null +++ b/.agents/skills/design-tokens/SKILL.md @@ -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. diff --git a/.agents/skills/motion/SKILL.md b/.agents/skills/motion/SKILL.md new file mode 100644 index 0000000..8f4f380 --- /dev/null +++ b/.agents/skills/motion/SKILL.md @@ -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. diff --git a/.agents/skills/verify-contract/SKILL.md b/.agents/skills/verify-contract/SKILL.md new file mode 100644 index 0000000..656d912 --- /dev/null +++ b/.agents/skills/verify-contract/SKILL.md @@ -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`); +``` diff --git a/.agents/skills/visual-regression/SKILL.md b/.agents/skills/visual-regression/SKILL.md new file mode 100644 index 0000000..a56d574 --- /dev/null +++ b/.agents/skills/visual-regression/SKILL.md @@ -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. diff --git a/.agents/templates/components/grid-group.astro b/.agents/templates/components/grid-group.astro new file mode 100644 index 0000000..b74ae57 --- /dev/null +++ b/.agents/templates/components/grid-group.astro @@ -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> diff --git a/.agents/templates/components/island.astro b/.agents/templates/components/island.astro new file mode 100644 index 0000000..dc50ecc --- /dev/null +++ b/.agents/templates/components/island.astro @@ -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> diff --git a/.agents/templates/components/static-block.astro b/.agents/templates/components/static-block.astro new file mode 100644 index 0000000..c5845da --- /dev/null +++ b/.agents/templates/components/static-block.astro @@ -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> diff --git a/.agents/templates/config/.prettierignore b/.agents/templates/config/.prettierignore new file mode 100644 index 0000000..4bbd663 --- /dev/null +++ b/.agents/templates/config/.prettierignore @@ -0,0 +1,7 @@ +dist +node_modules +public/hands-on +submitted-skills +skill-reviews +vote-service +.agents/snapshots diff --git a/.agents/templates/config/.prettierrc.json b/.agents/templates/config/.prettierrc.json new file mode 100644 index 0000000..596e8f0 --- /dev/null +++ b/.agents/templates/config/.prettierrc.json @@ -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 } } + ] +} diff --git a/.agents/templates/config/.stylelintrc.json b/.agents/templates/config/.stylelintrc.json new file mode 100644 index 0000000..e48c43d --- /dev/null +++ b/.agents/templates/config/.stylelintrc.json @@ -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" + } +} diff --git a/.agents/templates/config/eslint.config.js b/.agents/templates/config/eslint.config.js new file mode 100644 index 0000000..3b3240b --- /dev/null +++ b/.agents/templates/config/eslint.config.js @@ -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', + }, + }, +]; diff --git a/.agents/templates/config/gitea-ci.yaml b/.agents/templates/config/gitea-ci.yaml new file mode 100644 index 0000000..c3c1952 --- /dev/null +++ b/.agents/templates/config/gitea-ci.yaml @@ -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/ diff --git a/.agents/templates/config/package.scripts.json b/.agents/templates/config/package.scripts.json new file mode 100644 index 0000000..2b8688b --- /dev/null +++ b/.agents/templates/config/package.scripts.json @@ -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" + } +} diff --git a/.agents/templates/pages/chapter.astro b/.agents/templates/pages/chapter.astro new file mode 100644 index 0000000..8decd0d --- /dev/null +++ b/.agents/templates/pages/chapter.astro @@ -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> diff --git a/.agents/templates/pages/interactive.astro b/.agents/templates/pages/interactive.astro new file mode 100644 index 0000000..31ad48b --- /dev/null +++ b/.agents/templates/pages/interactive.astro @@ -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> diff --git a/.gitignore b/.gitignore index 3218536..da58ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..ea4a11a --- /dev/null +++ b/.husky/commit-msg @@ -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 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..ac60697 --- /dev/null +++ b/.husky/pre-commit @@ -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 diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..fbc3a35 --- /dev/null +++ b/.husky/pre-push @@ -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 diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..dbaa4bf --- /dev/null +++ b/.lintstagedrc.json @@ -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" + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..16f504e --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/plans/astro-refactor/MODEL-ROUTING.md b/plans/astro-refactor/MODEL-ROUTING.md new file mode 100644 index 0000000..28f3f37 --- /dev/null +++ b/plans/astro-refactor/MODEL-ROUTING.md @@ -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. diff --git a/plans/astro-refactor/README.md b/plans/astro-refactor/README.md new file mode 100644 index 0000000..59a02e3 --- /dev/null +++ b/plans/astro-refactor/README.md @@ -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. diff --git a/plans/astro-refactor/task-01-scaffold.md b/plans/astro-refactor/task-01-scaffold.md new file mode 100644 index 0000000..e958008 --- /dev/null +++ b/plans/astro-refactor/task-01-scaffold.md @@ -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. diff --git a/plans/astro-refactor/task-02-tokens.md b/plans/astro-refactor/task-02-tokens.md new file mode 100644 index 0000000..5743437 --- /dev/null +++ b/plans/astro-refactor/task-02-tokens.md @@ -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. diff --git a/plans/astro-refactor/task-03-verification-net.md b/plans/astro-refactor/task-03-verification-net.md new file mode 100644 index 0000000..fdbc6d3 --- /dev/null +++ b/plans/astro-refactor/task-03-verification-net.md @@ -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. diff --git a/plans/astro-refactor/task-04-content-schema.md b/plans/astro-refactor/task-04-content-schema.md new file mode 100644 index 0000000..b65472b --- /dev/null +++ b/plans/astro-refactor/task-04-content-schema.md @@ -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. diff --git a/plans/astro-refactor/task-05-content-guide.md b/plans/astro-refactor/task-05-content-guide.md new file mode 100644 index 0000000..00c35dc --- /dev/null +++ b/plans/astro-refactor/task-05-content-guide.md @@ -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 diff --git a/plans/astro-refactor/task-06-content-review.md b/plans/astro-refactor/task-06-content-review.md new file mode 100644 index 0000000..a9796fe --- /dev/null +++ b/plans/astro-refactor/task-06-content-review.md @@ -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) diff --git a/plans/astro-refactor/task-07-primitives.md b/plans/astro-refactor/task-07-primitives.md new file mode 100644 index 0000000..e23e335 --- /dev/null +++ b/plans/astro-refactor/task-07-primitives.md @@ -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`. diff --git a/plans/astro-refactor/task-08-route-cards.md b/plans/astro-refactor/task-08-route-cards.md new file mode 100644 index 0000000..1cbaca5 --- /dev/null +++ b/plans/astro-refactor/task-08-route-cards.md @@ -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. diff --git a/plans/astro-refactor/task-09-chapter-blocks.md b/plans/astro-refactor/task-09-chapter-blocks.md new file mode 100644 index 0000000..855cccc --- /dev/null +++ b/plans/astro-refactor/task-09-chapter-blocks.md @@ -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 diff --git a/plans/astro-refactor/task-10-guide-blocks.md b/plans/astro-refactor/task-10-guide-blocks.md new file mode 100644 index 0000000..06e1917 --- /dev/null +++ b/plans/astro-refactor/task-10-guide-blocks.md @@ -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 diff --git a/plans/astro-refactor/task-11-review-blocks.md b/plans/astro-refactor/task-11-review-blocks.md new file mode 100644 index 0000000..3c5da32 --- /dev/null +++ b/plans/astro-refactor/task-11-review-blocks.md @@ -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 diff --git a/plans/astro-refactor/task-12-page-landing.md b/plans/astro-refactor/task-12-page-landing.md new file mode 100644 index 0000000..3e72e25 --- /dev/null +++ b/plans/astro-refactor/task-12-page-landing.md @@ -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 diff --git a/plans/astro-refactor/task-13-page-chapters.md b/plans/astro-refactor/task-13-page-chapters.md new file mode 100644 index 0000000..8051498 --- /dev/null +++ b/plans/astro-refactor/task-13-page-chapters.md @@ -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. diff --git a/plans/astro-refactor/task-14-page-rules.md b/plans/astro-refactor/task-14-page-rules.md new file mode 100644 index 0000000..08af687 --- /dev/null +++ b/plans/astro-refactor/task-14-page-rules.md @@ -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 diff --git a/plans/astro-refactor/task-15-page-full-guide.md b/plans/astro-refactor/task-15-page-full-guide.md new file mode 100644 index 0000000..fbb08f6 --- /dev/null +++ b/plans/astro-refactor/task-15-page-full-guide.md @@ -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 diff --git a/plans/astro-refactor/task-16-page-review-desk.md b/plans/astro-refactor/task-16-page-review-desk.md new file mode 100644 index 0000000..4c64771 --- /dev/null +++ b/plans/astro-refactor/task-16-page-review-desk.md @@ -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 diff --git a/plans/astro-refactor/task-17-hands-on.md b/plans/astro-refactor/task-17-hands-on.md new file mode 100644 index 0000000..ffd1e70 --- /dev/null +++ b/plans/astro-refactor/task-17-hands-on.md @@ -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. diff --git a/plans/astro-refactor/task-18-motion.md b/plans/astro-refactor/task-18-motion.md new file mode 100644 index 0000000..3eb671d --- /dev/null +++ b/plans/astro-refactor/task-18-motion.md @@ -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 diff --git a/plans/astro-refactor/task-19-verify-repoint.md b/plans/astro-refactor/task-19-verify-repoint.md new file mode 100644 index 0000000..2e6168b --- /dev/null +++ b/plans/astro-refactor/task-19-verify-repoint.md @@ -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. diff --git a/plans/astro-refactor/task-20-cutover.md b/plans/astro-refactor/task-20-cutover.md new file mode 100644 index 0000000..d916ffd --- /dev/null +++ b/plans/astro-refactor/task-20-cutover.md @@ -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