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