chore: merge main into task 19

This commit is contained in:
Marcos Paulo
2026-09-06 00:35:03 +00:00
6 changed files with 882 additions and 55 deletions
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env node
// Compare the *rendered* text of a legacy page against its Astro replacement.
//
// node .agents/scripts/rendered-text-diff.mjs full-guide
// node .agents/scripts/rendered-text-diff.mjs full-guide --pt
//
// Why this exists: scripts/verify.mjs reads the legacy files, so a migrated
// page can drop half its content and still pass the gate. Task 15d shipped
// /full-guide/ missing 86 rendered spans -- the entire verification section,
// the hands-on exercise brief, and both "Clone from Gitea" links -- and every
// check was green.
//
// Static HTML comparison is useless here: the guide's tab panels are injected
// by an island at runtime, so half the legacy page's markup has no static
// counterpart. This walks the live DOM instead and skips anything the browser
// is not painting -- which also drops the hidden Portuguese half of each
// bilingual pair, so the two sides line up.
//
// Requires playwright (devDependency) and two static servers; it starts both.
import { spawn } from 'node:child_process';
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createServer } from 'node:net';
import { chromium } from 'playwright';
// `index` is the landing page: it lives at the repository root, not in a
// directory of its own, so it needs a different path on the legacy side.
const route = process.argv[2];
if (!route) {
console.error('usage: rendered-text-diff.mjs <route> [--pt] e.g. full-guide, or index');
process.exit(2);
}
// `--pt` clicks the language toggle on both pages first. English parity is
// only half the contract: a page can render every English string and still
// leave a restored block untranslated, because the Portuguese half is a
// separate set of nodes. Only /full-guide/ and /rules/ have a toggle.
const portuguese = process.argv.includes('--pt');
const legacyPath = route === 'index' ? 'index.html' : `${route}/index.html`;
const astroPath = route === 'index' ? '' : `${route}/`;
// The built site expects to be served under the configured base path.
const staging = mkdtempSync(join(tmpdir(), 'af-rtd-'));
cpSync('dist', join(staging, 'ai-for-dummies'), { recursive: true });
// Ask the kernel for a free port rather than pinning one. Back-to-back runs
// used to collide: the previous run's server was still holding the fixed port
// while its staging directory had already been deleted, so every page came
// back as a 404 and the diff reported the whole route missing.
const freePort = () =>
new Promise((resolve, reject) => {
const probe = createServer();
probe.on('error', reject);
probe.listen(0, '127.0.0.1', () => {
const { port } = probe.address();
probe.close(() => resolve(port));
});
});
const legacyPort = await freePort();
const astroPort = await freePort();
const serve = (dir, port) =>
spawn('python3', ['-m', 'http.server', String(port), '-d', dir], { stdio: 'ignore' });
const servers = [serve('.', legacyPort), serve(staging, astroPort)];
const stop = () => {
servers.forEach((s) => s.kill());
rmSync(staging, { recursive: true, force: true });
};
// Visible text nodes, in document order, whitespace collapsed.
const visibleText = () => {
const out = [];
const walk = (node) => {
for (const child of node.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.replace(/\s+/g, ' ').trim();
if (text) out.push(text);
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) continue;
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE') continue;
const style = getComputedStyle(child);
if (child.hidden || style.display === 'none' || style.visibility === 'hidden') continue;
walk(child);
}
};
walk(document.body);
return out;
};
try {
await new Promise((r) => setTimeout(r, 1500));
const browser = await chromium.launch();
const grab = async (url) => {
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
const response = await page.goto(url, { waitUntil: 'load' });
// A 404 renders as four spans of python's error page and the diff then
// reports the entire route as missing, which reads exactly like a real
// regression. Fail loudly instead.
if (!response || !response.ok()) {
throw new Error(`${url} returned ${response ? response.status() : 'no response'}`);
}
// The islands hydrate and render their initial panel on load; without this
// every panel's copy reads as missing.
await page.waitForTimeout(1200);
if (portuguese) {
const toggle = await page.$('[data-lang="pt"]');
if (!toggle) throw new Error(`no language toggle on ${url}`);
await toggle.click();
await page.waitForTimeout(1200);
}
// Islands hydrate at their own pace, and the language toggle repaints in
// more than one frame. A single read after a fixed wait is flaky, so read
// until two consecutive reads agree.
let spans = await page.evaluate(visibleText);
for (let i = 0; i < 10; i += 1) {
await page.waitForTimeout(300);
const next = await page.evaluate(visibleText);
if (next.length === spans.length && next.every((span, j) => span === spans[j])) {
spans = next;
break;
}
spans = next;
}
await page.close();
return spans;
};
const legacy = await grab(`http://localhost:${legacyPort}/${legacyPath}`);
const astro = await grab(`http://localhost:${astroPort}/ai-for-dummies/${astroPath}`);
await browser.close();
// Count occurrences, not membership. A set comparison reports zero when a
// string the legacy page paints four times is painted three times here --
// exactly the kind of near-miss that got past the earlier checks.
const tally = (spans) => {
const counts = new Map();
for (const span of spans) counts.set(span, (counts.get(span) || 0) + 1);
return counts;
};
const legacyCounts = tally(legacy);
const astroCounts = tally(astro);
const missing = [];
for (const [span, count] of legacyCounts) {
const short = count - (astroCounts.get(span) || 0);
for (let i = 0; i < short; i += 1) missing.push(span);
}
// Both directions. A string the Astro page paints and the legacy page does
// not is just as wrong: it means a translation was invented, or an English
// string was left standing where the legacy page swaps it.
const extra = [];
for (const [span, count] of astroCounts) {
const over = count - (legacyCounts.get(span) || 0);
for (let i = 0; i < over; i += 1) extra.push(span);
}
// Order counts too. Both pages can paint the same strings while a block
// sits in the wrong place -- the Portuguese eyebrow, or a reordered card
// deck -- and a count-only comparison calls that clean.
const firstOutOfOrder = legacy.findIndex((span, i) => astro[i] !== span);
const mode = portuguese ? 'pt' : 'en';
console.log(
`${mode} · legacy ${legacy.length} spans · astro ${astro.length} spans · missing ${missing.length} · extra ${extra.length}`,
);
for (const span of missing) console.log(` - ${span}`);
for (const span of extra) console.log(` + ${span}`);
if (firstOutOfOrder !== -1) {
console.log(` order diverges at span ${firstOutOfOrder}`);
console.log(` legacy: ${legacy[firstOutOfOrder]}`);
console.log(` astro: ${astro[firstOutOfOrder]}`);
}
process.exitCode = missing.length === 0 && extra.length === 0 && firstOutOfOrder === -1 ? 0 : 1;
} finally {
stop();
}
@@ -0,0 +1,293 @@
# Task 15f — Restore the content 15d dropped from /full-guide/
**Agent**: `page-migrator` · **Model**: Codex **Depends on**: 15d **Blocks**:
19, 20 **Worktree**: `.agents/scripts/worktree.sh start 15f full-guide-restore`
## Goal
`/full-guide/` is missing about a fifth of the page. Put it back, in the right
place, bilingual where the legacy page is bilingual.
Task 15d assembled the page and passed every check. It also dropped 87 rendered
text spans. One of them — the `.chapter-route` section, the only route out of
the guide to the summary, models, agents and skills chapters — has already been
restored (`c2b35d5`). The remaining 86 are your task.
Nothing caught this. `scripts/verify.mjs` reads `full-guide/index.html`, the
legacy file, which still has every one of these sections. The gate was green the
whole time. Task 19 is blocked on you: it re-pointed its assertions at `dist/`
and they now fail, correctly, on exactly this content.
## How to see the gap
```
pnpm run build
node .agents/scripts/rendered-text-diff.mjs full-guide
```
It walks the live DOM of both pages and prints the text the legacy page paints
and the Astro page does not. It skips hidden nodes, so the Portuguese half of
each bilingual pair does not register as a difference, and it waits for the
islands to hydrate, so the tab panels' injected copy counts as present.
**This script reaching zero is the task.** Do not edit it to make it pass.
## What is missing
Whole blocks, not scattered strings. By CSS class, present in
`full-guide/index.html` and absent from `dist/full-guide/index.html`:
`verify-intro`, `verify-cta`, `verify-cta-grid`, `verify-card`,
`verify-card-source`, `verify-antipatterns`, `ap-grid`, `comparison-strip`,
`exercise-brief`, `builder-intro`, `builder-loop`, `builder-artifact`,
`artifact-head`, `artifact-command`, `starter-link-group`, `starter-link-source`
That covers, at minimum: the three-layer verification section and its code
lines, the "four ways a green report is false" grid, the two hands-on lab cards
with both "Clone from Gitea →" links, the exercise brief (stack / dependencies /
files), the four-row comparison strip, and the skill-forge output package tree
with its validate and after-real-use panels.
The exact 86 spans, in document order:
```
- default start
- name: review-ui · check focus, mobile, reduced motion · run verification · return evidence
- The skill forge
- Teach the decision.
- Keep the context
- light.
- Do not package everything you know. Capture the non-obvious choices that repeatedly improve an outcome, then prove the skill changes behavior.
- Observe
- find repeated friction
- Define trigger
- route precisely
- Choose anatomy
- only needed files
- Write guidance
- decisions, not trivia
- Validate
- test real behavior
- OUTPUT / SKILL PACKAGE
- review-ui/
- ├── SKILL.md
- ├── agents/
- │ └── openai.yaml
- ├── references/
- │ └── accessibility.md
- └── scripts/
- └── verify.mjs
- VALIDATE
- quick_validate.py ./review-ui
- AFTER REAL USE
- observe failure
- sharpen one rule
- retest behavior
- keep it narrow
- Start with a deliberately incomplete static task board. Run one prompt as written, reset, then run the skill-enabled version. Compare diff size, verification evidence, and unnecessary complexity.
- Clone from Gitea →
- Clone from Gitea →
- THE MISSING FEATURE
- Add All / Open / Done filters that survive reload and browser navigation.
- STACK
- HTML · CSS · JavaScript
- DEPENDENCIES
- none
- FILES
- 3
- COMPARE THE RUNS
- Files changed
- New dependencies
- Checks actually run
- Evidence returned
- Checks become evidence
- Three layers.
- Run each one alone.
- Run a gate on its own line, print its exit code, attach the output. The result is the deliverable.
- Format, lint, type-check. Fast and scoped to one file. Run on every save.
- pnpm lint; echo "lint=$?" pnpm typecheck; echo "typecheck=$?"
- pnpm test; echo "test=$?" cd services/api && go test ./...
- Drive the actual UI, API, or browser. Slower and flakier — only this catches mobile overflow and a missing 404.
- pnpm check:ui; echo "ui=$?" TURBO_FORCE=true pnpm e2e
- FOUR WAYS A GREEN REPORT IS FALSE
- 1
- Pipe a gate
- tail, grep, or head hide the real exit code — a pipeline returns the last command's status.
- 2
- Swallow a rejection
- A silent
- .catch(() => {})
- hides a panic, an upstream limit, or a partial failure.
- 3
- Trust the cache
- Turbo caches results. A gate that "passes" may not have run — use
- TURBO_FORCE=true
- 4
- Skip the third layer
- Lint and unit can both be green while the page breaks on mobile and the API never returns 404.
- RUN IT YOURSELF · two labs, under 10 minutes each
- Path A · verification lab
- Fill the four-row comparison strip on the starter. Run A naively, Run B with
- $gate-discipline
- and
- $webapp-testing
- Clone ↗
- git.marcospaulo.dev.br/.../src/branch/pages/hands-on/starter
- Path B · rules lab
- Toggle every rule off, run the prompt. Toggle every rule on, run it again. Compare diff size, gate invocations, and the names of checks the agent names back.
- Clone ↗
- git.marcospaulo.dev.br/.../src/branch/pages/hands-on/rules
```
## Method
1. Read the legacy source for each block out of `full-guide/index.html`. Copy
the strings; do not retype them. Several contain box-drawing characters
(`├──`, `└──`), `·` separators, and `$`-prefixed skill names.
2. Place each block where the legacy page has it — the section order is part of
the argument the page is making.
3. Bilingual pairs follow `.agents/rules/content-i18n.md`: render the fragment
twice, `data-language-content="en"` visible and `data-language-content="pt"`
hidden. **Check `translations.pt` in `app.js` before assuming a block is
bilingual.** Several of these are English-only on the live site —
`.chapter-route` was — and inventing Portuguese for them is a regression in
the other direction.
4. Reuse the existing blocks in `src/components/blocks/`. If a block does not
exist, this is assembly work that 15d should have done and you may write the
markup inline in the page, as 15d did elsewhere. Do not write a new island.
## Do not
- Do not touch `full-guide/index.html`, `app.js`, or any other legacy file.
- Do not weaken or delete an assertion in `scripts/verify.mjs`.
- Do not reformat files you are not restoring content into. 15d ran prettier
across the whole repo on one attempt and it had to be reverted.
## Done when
- [ ] `node .agents/scripts/rendered-text-diff.mjs full-guide` reports 0 missing
- [ ] Every restored bilingual block has both `en` and `pt`; every English-only
block is English-only in `translations.pt` too, and you say which is which
- [ ] Section order matches the legacy page
- [ ] `pnpm run gate` green
## Attempt 1: English is exact, Portuguese is not
`8f82304`, tagged `rejected/15f-attempt-1`. Keep it and continue from it — the
English restoration is correct and complete:
```
en · legacy 432 spans · astro 432 spans · missing 0 · extra 0
```
Every block is back, in the legacy order, with no invented content. That half of
the task is done.
The Portuguese half was not checked, because the tool could not check it when
you started. `rendered-text-diff.mjs` now takes `--pt`: it clicks the language
toggle on both pages before reading, and it reports **both** directions — a
string the Astro page paints and the legacy page does not is as wrong as one it
drops. On your branch:
```
pt · legacy 431 spans · astro 431 spans · missing 27 · extra 26
```
The totals match because nothing is structurally missing. The content is English
where the legacy page shows Portuguese.
### Two separate causes — fix both
**1. The blocks you restored are English-only under PT.** "Create a skill",
"INSTALL PACK", "Hands-on", "Tiny Tasks lab", "Good prompt", both long lab
prompts, the two `Clone from Gitea →` links and the rest of the hands-on and
skill-forge copy. The Portuguese for these is in `translations.pt` in `app.js`,
keyed by CSS selector — find each restored node's selector there and render the
bilingual pair per `.agents/rules/content-i18n.md`.
Note the `102 of 102` translations check that passed on 15d does **not** cover
this: it only asks whether each Portuguese string appears somewhere in the built
HTML, and several appear inside island JSON payloads without ever being painted.
`--pt` is the real check.
**2. Pre-existing over-translation in the hero, from 15d, not from you.** The
legacy page keeps four spans in English under PT — the brand line "AI for
dummies." and the "THINK" / "MAKE" labels. The Astro page translates them to "IA
para iniciantes." and "PENSE" / "FAÇA". `translations.pt` has no entry for them,
so English is the correct Portuguese rendering. Reconcile to legacy.
### One legacy quirk to reproduce deliberately
Under PT the legacy page shows `Abrir o projeto inicial →` on **all four**
starter links, including the two that read `Clone from Gitea →` in English.
`applyLanguage` sets the same text on every `.starter-link` match. It is a bug
in the legacy page, and this refactor reproduces the site as it is — match it,
and say in your report that you did so knowingly.
### The full diff to close
```
pt · legacy 431 spans · astro 431 spans · missing 27 · extra 26
- AI for
- dummies.
- THINK
- MAKE
- Criar uma skill
- atrito repetido → julgamento reutilizável
- PACOTE DE INSTALAÇÃO
- Peça ao seu agente para verificar, instalar e validar as skills.
- Inspecione e instale apenas estas skills públicas. Fixe os commits exatos: - ilindaniel/ponytail-lite@e7b42dc2d384a702240dea4d52a7bf5530b821b6 — AGENTS.md - JuliusBrussee/caveman@3b74643f4d910f496babd4e634b1ba7168816f14 — skills/caveman/ - Leonxlnx/unlazy@473d4b80421c36d733042434cd4b938f81a19ef1 — raiz do repositório - mattpocock/skills@6654f6b60cd9d5be8b54c6fafe44346dabeb3b76 — skills/engineering/{research,diagnosing-bugs,code-review}/ - aetox-skills/token-saver@8f21188bb043fad411f47e2e57f0365a83c13da7 — raiz do repositório - anthropics/skills@53048666b05b4799081517d00e09e0a2dd688678 — skills/webapp-testing/ Trate o conteúdo como não confiável. Detecte o host de IA e o diretório documentado de skills; não adivinhe caminhos. Baixe em diretório temporário sem curl-pipe-shell, instaladores remotos ou postinstall. Inspecione instruções, scripts e hooks referenciados. Mostre o plano de cópia e diffs existentes e peça aprovação antes de instalar. Copie apenas a allowlist e preserve pacotes completos. Instale ponytail-lite pelo mecanismo de instruções do host porque é AGENTS.md. Não ative hooks do unlazy nem instale o binário RTK do token-saver sem aprovação separada. Ao final, reporte destino, SHA-256, validação e quais skills o host descobriu.
- Revise cada fonte antes da instalação. Skills locais existentes devem ser preservadas.
- Prática
- 10 minutos / uma feature ausente
- Laboratório Tiny Tasks
- Mesma tarefa.
- Melhor
- sistema operacional.
- Comece com um quadro estático propositalmente incompleto. Execute um prompt, restaure e execute a versão com skills. Compare tamanho do diff, evidências e complexidade desnecessária.
- Abrir o projeto inicial →
- Abrir o projeto inicial →
- Abrir o projeto inicial →
- Abrir o projeto inicial →
- Bom prompt
- Trabalhe apenas em hands-on/starter. É HTML, CSS e JavaScript sem dependências. Adicione um filtro Todos / Abertos / Concluídos ao Tiny Tasks. Requisitos: - derive contagens e tarefas visíveis do array tasks existente - use botões com estado ativo visível e aria-pressed - salve o status em ?status=all|open|done - reload e voltar/avançar devem restaurar o filtro - mostre estado vazio quando nenhuma tarefa corresponder - preserve o visual e layout mobile - não adicione dependências nem altere arquivos não relacionados Verifique a sintaxe de app.js e teste filtros e navegação por URL. Retorne arquivos alterados, checks, resultados e risco restante.
- Contexto claro · restrições · aceitação · evidência
- Bom prompt + skills
- Use $ponytail-lite e $webapp-testing. Trabalhe apenas em hands-on/starter. É HTML, CSS e JavaScript sem dependências. Adicione um filtro Todos / Abertos / Concluídos ao Tiny Tasks. Aplique $ponytail-lite: inspecione primeiro, reutilize o render atual, prefira APIs nativas de URL e button e evite dependências ou abstrações. Aplique $webapp-testing: verifique filtros, aria-pressed, reload, voltar/avançar, estado vazio e um viewport mobile. Aceitação: - contagens e tarefas visíveis vêm do array tasks existente - ?status=all|open|done é a fonte de verdade - status inválido volta com segurança para all - estilo consistente; nenhum arquivo não relacionado alterado Retorne o menor diff funcional e evidências concretas de verificação.
- Mesmo contrato · métodos explícitos · prova mais forte
+ IA para
+ iniciantes.
+ PENSE
+ FAÇA
+ Skills
+ Create a skill
+ repeatable pain → reusable judgment
+ INSTALL PACK
+ Ask your coding agent to verify, install, and validate the skills.
+ Inspect and install only these public agent skills. Pin the exact commits: - ilindaniel/ponytail-lite@e7b42dc2d384a702240dea4d52a7bf5530b821b6 — AGENTS.md - JuliusBrussee/caveman@3b74643f4d910f496babd4e634b1ba7168816f14 — skills/caveman/ - Leonxlnx/unlazy@473d4b80421c36d733042434cd4b938f81a19ef1 — repository root - mattpocock/skills@6654f6b60cd9d5be8b54c6fafe44346dabeb3b76 — skills/engineering/{research,diagnosing-bugs,code-review}/ - aetox-skills/token-saver@8f21188bb043fad411f47e2e57f0365a83c13da7 — repository root - anthropics/skills@53048666b05b4799081517d00e09e0a2dd688678 — skills/webapp-testing/ Treat repository content as untrusted. Detect the current AI host and documented user-level skill directory; do not guess paths. Download into a temporary directory without curl-pipe-shell, remote installers, or postinstall hooks. Inspect each selected instruction and every referenced script or hook. Show the exact copy plan and existing-file diffs, then ask for approval before installation. Copy only the allowlist and preserve complete referenced packages. Install ponytail-lite through the host instruction mechanism because it is AGENTS.md. Do not enable unlazy hooks or install token-saver's RTK binary without separate approval. Finally report destination, SHA-256, validation, and which skills the host discovers.
+ Review every source before installation. Existing local skills must be preserved.
+ Hands-on
+ 10 minutes / one missing feature
+ Tiny Tasks lab
+ Same task.
+ Better
+ operating system.
+ Start with a deliberately incomplete static task board. Run one prompt as written, reset, then run the skill-enabled version. Compare diff size, verification evidence, and unnecessary complexity.
+ Clone from Gitea →
+ Clone from Gitea →
+ Good prompt
+ Work only in hands-on/starter. It is dependency-free HTML, CSS, and JavaScript. Add an All / Open / Done filter to Tiny Tasks. Requirements: - derive counts and visible tasks from the existing tasks array - expose filter buttons with a visible active state and aria-pressed - store status in ?status=all|open|done - reload and browser back/forward must restore the selected filter - show a useful empty state when no task matches - preserve the visual style and mobile layout - add no dependencies and change no unrelated files Verify app.js syntax and exercise every filter plus URL navigation. Return changed files, checks run, results, and remaining risk.
+ Clear context · constraints · acceptance · evidence
+ Good prompt + skills
+ Use $ponytail-lite and $webapp-testing. Work only in hands-on/starter. It is dependency-free HTML, CSS, and JavaScript. Add an All / Open / Done filter to Tiny Tasks. Apply $ponytail-lite: inspect first, reuse the current render flow, prefer native URL and button APIs, and avoid dependencies or abstractions. Apply $webapp-testing: verify all filters, aria-pressed, reload, browser back/forward, empty state, and one mobile viewport. Acceptance: - counts and visible tasks come from the existing tasks array - ?status=all|open|done is the source of truth - invalid status falls back safely to all - style remains consistent; unrelated files remain untouched Return the smallest working diff and concrete verification evidence.
+ Same contract · explicit working methods · stronger proof
```
## Done when (attempt 2)
- [ ] `node .agents/scripts/rendered-text-diff.mjs full-guide`
`missing 0 · extra 0`
- [ ] `node .agents/scripts/rendered-text-diff.mjs full-guide --pt`
`missing 0 · extra 0`
- [ ] No legacy file touched; no assertion in `scripts/verify.mjs` touched
- [ ] `pnpm run gate` green
+2 -1
View File
@@ -121,7 +121,8 @@ const { target } = Astro.props;
if (!id) return;
const target = document.querySelector('#' + id);
if (!target) return;
const value = target.textContent || '';
const visible = target.querySelector(':scope > [data-language-content]:not([hidden])');
const value = visible?.textContent || target.textContent || '';
if (
typeof navigator !== 'undefined' &&
@@ -403,8 +403,13 @@ fix(api): scope session query</code></pre>
select('[data-skill]', id, 'skill');
}
// Keys whose copy carries markup, so the swap must write innerHTML.
// `skillsText` was missing while the markup renders it with `set:html`:
// the island rewrote it as text on load and every visitor saw a literal
// `<code>.agents/skills/</code>` in the paragraph.
const HTML_KEYS = {
heroTitle: 1,
skillsText: 1,
pipelineTitle: 1,
skillsTitle: 1,
ratchetTitle: 1,
+1 -3
View File
@@ -56,7 +56,6 @@
"skillReadSkill": "READ SKILL ↗",
"skillReadSkillPt": "LER SKILL ↗",
"skillTriggerLabel": "TRIGGER",
"skillTriggerLabelPt": "GATILHO",
"copyStatus": "Prompt copied.",
"copyStatusPt": "Prompt copiado.",
"copyFallback": "Select the text manually.",
@@ -118,8 +117,7 @@
"stageOpenSourcePt": "ABRIR FONTE ↗",
"skillReadSkill": "LER SKILL ↗",
"skillReadSkillPt": "LER SKILL ↗",
"skillTriggerLabel": "TRIGGER",
"skillTriggerLabelPt": "GATILHO",
"skillTriggerLabel": "GATILHO",
"copyStatus": "Prompt copied.",
"copyStatusPt": "Prompt copiado.",
"copyFallback": "Select the text manually.",
+388 -39
View File
@@ -55,6 +55,9 @@ const efforts = toRecord(effortEntries);
const skillFiles = toRecord(skillFileEntries);
const skillWorkflow = toRecord(workflowEntries);
const commonSkills = toRecord(commonSkillEntries);
const sortedCommonSkills = Object.values(commonSkills).sort((a, b) =>
a.number.localeCompare(b.number),
);
const handsOn = promptEntries.find(({ data }) => data.id === 'tiny-tasks')?.data;
const install = installEntries.find(({ data }) => data.id === 'install')?.data;
if (!handsOn || !install) throw new Error('full-guide content collections are incomplete');
@@ -82,6 +85,31 @@ const labels = {
source: { en: 'GITHUB SOURCE ↗', pt: 'FONTE NO GITHUB ↗' },
},
};
const builderSteps: Record<
string,
{ title: { en: string; pt: string }; tagline: { en: string; pt: string } }
> = {
observe: {
title: { en: 'Observe', pt: 'Observar' },
tagline: { en: 'find repeated friction', pt: 'encontre atrito repetido' },
},
trigger: {
title: { en: 'Define trigger', pt: 'Definir gatilho' },
tagline: { en: 'route precisely', pt: 'roteie com precisão' },
},
scaffold: {
title: { en: 'Choose anatomy', pt: 'Escolher anatomia' },
tagline: { en: 'only needed files', pt: 'apenas arquivos necessários' },
},
write: {
title: { en: 'Write guidance', pt: 'Escrever orientação' },
tagline: { en: 'decisions, not trivia', pt: 'decisões, não trivialidades' },
},
validate: {
title: { en: 'Validate', pt: 'Validar' },
tagline: { en: 'test real behavior', pt: 'teste comportamento real' },
},
};
const selectorData = {
phases,
workers,
@@ -235,7 +263,7 @@ const base = import.meta.env.BASE_URL;
<section class="hero" id="top-pt" data-language-content="pt" hidden>
<div>
<p class="eyebrow">Uma apresentação para quem entrega software</p><h1>
IA para<br /><em>iniciantes.</em>
AI for<br /><em>dummies.</em>
</h1><p class="lede">
Você não precisa de um exército de modelos. Precisa de um sistema: uma mente para
enquadrar o trabalho, várias mãos para executá-lo e uma fronteira clara entre cada tarefa.
@@ -307,7 +335,7 @@ const base = import.meta.env.BASE_URL;
>Modelo forte para ambiguidade.<br />Modelo leve para trabalho delimitado.</strong
>
</div><div class="signal" aria-hidden="true">
<b>PENSE</b><i></i><i></i><i></i><b>FAÇA</b>
<b>THINK</b><i></i><i></i><i></i><b>MAKE</b>
</div>
</section>
@@ -418,7 +446,8 @@ const base = import.meta.env.BASE_URL;
data-language-content="pt"
hidden>Clique em uma fase.<br /><em>Veja a passagem.</em></span
>
</h2><p>
</h2><!-- The legacy PT render paints this paragraph twice; preserve that output. -->
<p>
<span data-language-content="en"
>Delegation means moving one bounded task into a smaller contextnot giving away
responsibility.</span
@@ -690,7 +719,16 @@ const base = import.meta.env.BASE_URL;
></small
></button
><button class="active" data-effort="medium" role="tab" aria-selected="true"
>MEDIUM</button
><b
><span data-language-content="en">MEDIUM</span><span data-language-content="pt" hidden
>MÉDIO</span
></b
><small
><span data-language-content="en">default start</span><span
data-language-content="pt"
hidden>ponto inicial</span
></small
></button
><button data-effort="high" role="tab" aria-selected="false"
><b
><span data-language-content="en">HIGH</span><span data-language-content="pt" hidden
@@ -731,8 +769,15 @@ const base = import.meta.env.BASE_URL;
<section class="skills" id="skills">
<div>
<p class="eyebrow">
{
/* Under Portuguese the legacy page paints the long skill sentence
here, not "Skills": its `.skills > div:first-child > p` selector
also matches this eyebrow and overwrites it. Reproduced on
purpose -- see .agents/rules/content-i18n.md. */
}
<span data-language-content="en">Skills</span><span data-language-content="pt" hidden
>Skills</span
>Uma skill é um procedimento reutilizável. Ela pode carregar instruções, referências,
scripts e assets. Não é memória mágica e não substitui critérios de aceitação.</span
>
</p><h2>
<span data-language-content="en">Write the right way<br /><em>once.</em></span><span
@@ -778,16 +823,51 @@ const base = import.meta.env.BASE_URL;
>
</div>
</article>
</div>
</div><pre><code>name: review-ui · check focus, mobile, reduced motion · run verification · return evidence</code></pre>
</section>
<section class="skill-builder" id="create-skill">
<div class="section-label">
<span>Create a skill</span><span>repeatable pain reusable judgment</span>
<span
><span data-language-content="en">Create a skill</span><span
data-language-content="pt"
hidden>Criar uma skill</span
></span
><span
><span data-language-content="en">repeatable pain reusable judgment</span><span
data-language-content="pt"
hidden>atrito repetido julgamento reutilizável</span
></span
>
</div><div class="builder-intro">
<div>
<p class="eyebrow">
<span data-language-content="en">The skill forge</span><span
data-language-content="pt"
hidden>A forja de skills</span
>
</p><h2>
<span data-language-content="en"
>Teach the decision.<br />Keep the context <em>light.</em></span
><span data-language-content="pt" hidden
>Ensine a decisão.<br />Mantenha o contexto <em>leve.</em></span
>
</h2>
</div><p>
<span data-language-content="en"
>Do not package everything you know. Capture the non-obvious choices that repeatedly
improve an outcome, then prove the skill changes behavior.</span
><span data-language-content="pt" hidden
>Não empacote tudo o que você sabe. Capture as escolhas não óbvias que melhoram
resultados repetidamente e prove que a skill muda o comportamento.</span
>
</p>
</div><div class="builder-workbench">
<nav class="builder-steps" role="tablist" aria-label="Skill creation workflow">
{
Object.values(skillWorkflow).map((step) => (
['observe', 'trigger', 'scaffold', 'write', 'validate'].map((id) => {
const step = skillWorkflow[id];
return (
<button
class:list={{ active: step.id === 'observe' }}
data-skill-step={step.id}
@@ -796,10 +876,28 @@ const base = import.meta.env.BASE_URL;
>
<>
<b>{step.number}</b>
<span>{step.title.en}</span>
<>
<span>
<>
<span data-language-content="en">{builderSteps[step.id].title.en}</span>
<span data-language-content="pt" hidden>
{builderSteps[step.id].title.pt}
</span>
</>
</span>
<small>
<>
<span data-language-content="en">{builderSteps[step.id].tagline.en}</span>
<span data-language-content="pt" hidden>
{builderSteps[step.id].tagline.pt}
</span>
</>
</small>
</>
</>
</button>
))
);
})
}
</nav><article class="builder-detail" id="builder-detail" aria-live="polite">
<header>
@@ -819,7 +917,39 @@ const base = import.meta.env.BASE_URL;
>
</div>
</footer>
</article>
</article><aside class="builder-artifact">
<div class="artifact-head">
<span
><span data-language-content="en">OUTPUT / SKILL PACKAGE</span><span
data-language-content="pt"
hidden>SAÍDA / PACOTE DE SKILL</span
></span
><i></i>
</div><pre
aria-label="Example skill structure"><code>review-ui/<br /> SKILL.md<br /> agents/<br /> openai.yaml<br /> references/<br /> accessibility.md<br /> scripts/<br /> verify.mjs</code></pre><div
class="artifact-command"
>
<span
><span data-language-content="en">VALIDATE</span><span
data-language-content="pt"
hidden>VALIDAR</span
></span
><code>quick_validate.py ./review-ui</code>
</div>
</aside>
</div><div class="builder-loop">
<span
><span data-language-content="en">AFTER REAL USE</span><span
data-language-content="pt"
hidden>APÓS USO REAL</span
></span
><div data-language-content="en">
<b>observe failure</b><i></i><b>sharpen one rule</b><i></i><b>retest behavior</b><i></i
><b>keep it narrow</b>
</div><div data-language-content="pt" hidden>
<b>observar falha</b><i></i><b>refinar uma regra</b><i></i><b>retestar comportamento</b
><i></i><b>manter estreita</b>
</div>
</div>
</section>
@@ -866,7 +996,10 @@ const base = import.meta.env.BASE_URL;
</div><div class="skill-deck">
<div class="skill-index" role="tablist" aria-label="Common agent skills">
{
Object.values(commonSkills).map((skill) => (
/* getCollection's order is not the deck's order. Sort by the
card number so the tabs stay 01..07 as the legacy page has
them, instead of shifting between builds. */
sortedCommonSkills.map((skill) => (
<button
class:list={{ active: skill.id === 'ponytail' }}
data-common-skill={skill.id}
@@ -934,53 +1067,184 @@ const base = import.meta.env.BASE_URL;
</div><article class="install-skills">
<header>
<div>
<span>INSTALL PACK</span><strong
>Ask your coding agent to verify, install, and validate the skills.</strong
<span
><span data-language-content="en">INSTALL PACK</span><span
data-language-content="pt"
hidden>PACOTE DE INSTALAÇÃO</span
></span
><strong
><span data-language-content="en"
>Ask your coding agent to verify, install, and validate the skills.</span
><span data-language-content="pt" hidden
>Peça ao seu agente para verificar, instalar e validar as skills.</span
></strong
>
</div><CopyPrompt target="prompt-install-skills" />
</header><pre><code id="prompt-install-skills">{install.en}</code></pre><footer>
Review every source before installation. Existing local skills must be preserved.
</header><pre><code id="prompt-install-skills"><span data-language-content="en">{install.en}</span><span data-language-content="pt" hidden>{install.pt}</span></code></pre><footer
>
<span data-language-content="en"
>Review every source before installation. Existing local skills must be preserved.</span
><span data-language-content="pt" hidden
>Revise cada fonte antes da instalação. Skills locais existentes devem ser preservadas.</span
>
</footer>
</article>
</section>
<section class="hands-on" id="hands-on">
<div class="section-label">
<span>Hands-on</span><span>10 minutes / one missing feature</span>
<span
><span data-language-content="en">Hands-on</span><span data-language-content="pt" hidden
>Prática</span
></span
><span
><span data-language-content="en">10 minutes / one missing feature</span><span
data-language-content="pt"
hidden>10 minutos / uma feature ausente</span
></span
>
</div><div class="hands-intro">
<div>
<p class="eyebrow">Tiny Tasks lab</p><h2>
Same task.<br />Better <em>operating system.</em>
<p class="eyebrow">
<span data-language-content="en">Tiny Tasks lab</span><span
data-language-content="pt"
hidden>Laboratório Tiny Tasks</span
>
</p><h2>
<span data-language-content="en">Same task.<br />Better <em>operating system.</em></span
><span data-language-content="pt" hidden
>Mesma tarefa.<br />Melhor <em>sistema operacional.</em></span
>
</h2>
</div><div>
<p>
<p data-language-content="en">
Start with a deliberately incomplete static task board. Run one prompt as written,
reset, then run the skill-enabled version.
reset, then run the skill-enabled version. Compare diff size, verification evidence, and
unnecessary complexity.
</p><p data-language-content="pt" hidden>
Comece com um quadro estático propositalmente incompleto. Execute um prompt, restaure e
execute a versão com skills. Compare tamanho do diff, evidências e complexidade
desnecessária.
</p><div class="starter-links">
<a href={`${base}hands-on/starter/`} class="starter-link">Open the starter </a><a
href={`${base}hands-on/rules/`}
class="starter-link">Open the rules lab </a
<div class="starter-link-group">
<a href={`${base}hands-on/starter/`} class="starter-link"
><span data-language-content="en">Open the starter </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
><a
href="https://git.marcospaulo.dev.br/netcracker/ai-for-dummies/src/branch/pages/hands-on/starter"
class="starter-link starter-link-source"
><span data-language-content="en">Clone from Gitea </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
>
</div><div class="starter-link-group">
<a href={`${base}hands-on/rules/`} class="starter-link"
><span data-language-content="en">Open the rules lab </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
><a
href="https://git.marcospaulo.dev.br/netcracker/ai-for-dummies/src/branch/pages/hands-on/rules"
class="starter-link starter-link-source"
><span data-language-content="en">Clone from Gitea </span><span
data-language-content="pt"
hidden>Abrir o projeto inicial </span
></a
>
</div>
</div>
</div>
</div><div class="exercise-brief">
<span
><span data-language-content="en">THE MISSING FEATURE</span><span
data-language-content="pt"
hidden>A FEATURE AUSENTE</span
></span
><strong
><span data-language-content="en"
>Add All / Open / Done filters that survive reload and browser navigation.</span
><span data-language-content="pt" hidden
>Adicione filtros Todos / Abertos / Concluídos que sobrevivem reload e navegação.</span
></strong
><div>
<span data-language-content="en"
><b>STACK</b> HTML · CSS · JavaScript <b>DEPENDENCIES</b> none <b>FILES</b> 3</span
><span data-language-content="pt" hidden
><b>STACK</b> HTML · CSS · JavaScript <b>DEPENDÊNCIAS</b> nenhuma <b>ARQUIVOS</b> 3</span
>
</div>
</div><div class="prompt-compare">
<article class="prompt-card">
<header>
<div><span>RUN A</span><strong>Good prompt</strong></div><CopyPrompt
target="prompt-basic"
/>
</header><pre><code id="prompt-basic">{handsOn.en.basic}</code></pre><footer>
Clear context · constraints · acceptance · evidence
<div>
<span>RUN A</span><strong
><span data-language-content="en">Good prompt</span><span
data-language-content="pt"
hidden>Bom prompt</span
></strong
>
</div><CopyPrompt target="prompt-basic" />
</header><pre><code id="prompt-basic"><span data-language-content="en">{handsOn.en.basic}</span><span data-language-content="pt" hidden>{handsOn.pt.basic}</span></code></pre><footer
>
<span data-language-content="en"
>Clear context · constraints · acceptance · evidence</span
><span data-language-content="pt" hidden
>Contexto claro · restrições · aceitação · evidência</span
>
</footer>
</article><article class="prompt-card enhanced">
<header>
<div><span>RUN B</span><strong>Good prompt + skills</strong></div><CopyPrompt
target="prompt-skills"
/>
</header><pre><code id="prompt-skills">{handsOn.en.skills}</code></pre><footer>
Same contract · explicit working methods · stronger proof
<div>
<span>RUN B</span><strong
><span data-language-content="en">Good prompt + skills</span><span
data-language-content="pt"
hidden>Bom prompt + skills</span
></strong
>
</div><CopyPrompt target="prompt-skills" />
</header><pre><code id="prompt-skills"><span data-language-content="en">{handsOn.en.skills}</span><span data-language-content="pt" hidden>{handsOn.pt.skills}</span></code></pre><footer
>
<span data-language-content="en"
>Same contract · explicit working methods · stronger proof</span
><span data-language-content="pt" hidden
>Mesmo contrato · métodos explícitos · prova mais forte</span
>
</footer>
</article>
</div><div class="comparison-strip">
<span
><span data-language-content="en">COMPARE THE RUNS</span><span
data-language-content="pt"
hidden>COMPARE AS EXECUÇÕES</span
></span
><div>
<b>01</b>
<span data-language-content="en">Files changed</span><span
data-language-content="pt"
hidden>Arquivos alterados</span
>
</div><div>
<b>02</b>
<span data-language-content="en">New dependencies</span><span
data-language-content="pt"
hidden>Novas dependências</span
>
</div><div>
<b>03</b>
<span data-language-content="en">Checks actually run</span><span
data-language-content="pt"
hidden>Checks executados</span
>
</div><div>
<b>04</b>
<span data-language-content="en">Evidence returned</span><span
data-language-content="pt"
hidden>Evidências retornadas</span
>
</div>
</div><p class="copy-status" id="copy-status" role="status" aria-live="polite"></p>
</section>
@@ -1017,21 +1281,90 @@ const base = import.meta.env.BASE_URL;
<section class="verification" id="verification">
<div class="section-label">
<span>Verification</span><span>run each gate separately</span>
</div><div class="verify-intro">
<div>
<p class="eyebrow">Checks become evidence</p><h2>
Three layers.<br />Run each one alone.
</h2>
</div><p>
Run a gate on its own line, print its exit code, attach the output. The result is the
deliverable.
</p>
</div><div class="verify-layers">
<article>
<span>01 · STATIC</span><h3>Lint and types</h3><p>
Format, lint, type-check. Fast and scoped to one file.
</p><code>pnpm lint; echo "lint=$?"</code>
Format, lint, type-check. Fast and scoped to one file. Run on every save.
</p><code>pnpm lint; echo "lint=$?" pnpm typecheck; echo "typecheck=$?"</code>
</article><article>
<span>02 · BEHAVIOR</span><h3>Unit and contract</h3><p>
Tests that repeat. Run before claiming done.
</p><code>pnpm test; echo "test=$?"</code>
</p><code>pnpm test; echo "test=$?" cd services/api && go test ./...</code>
</article><article>
<span>03 · INTEGRATION</span><h3>Real UI and API</h3><p>
Drive the actual UI, API, or browser.
</p><code>pnpm check:ui; echo "ui=$?"</code>
Drive the actual UI, API, or browser. Slower and flakier only this catches mobile
overflow and a missing 404.
</p><code>pnpm check:ui; echo "ui=$?" TURBO_FORCE=true pnpm e2e</code>
</article>
</div><div class="verify-antipatterns">
<span>FOUR WAYS A GREEN REPORT IS FALSE</span><div class="ap-grid">
<article>
<b>1</b><div>
<strong>Pipe a gate</strong><p>
tail, grep, or head hide the real exit code a pipeline returns the last command's
status.
</p>
</div>
</article><article>
<b>2</b><div>
<strong>Swallow a rejection</strong><p>
A silent <code>{'.catch(() => {})'}</code> hides a panic, an upstream limit, or a partial
failure.
</p>
</div>
</article><article>
<b>3</b><div>
<strong>Trust the cache</strong><p>
Turbo caches results. A gate that "passes" may not have run use <code
>TURBO_FORCE=true</code
>.
</p>
</div>
</article><article>
<b>4</b><div>
<strong>Skip the third layer</strong><p>
Lint and unit can both be green while the page breaks on mobile and the API never
returns 404.
</p>
</div>
</article>
</div>
</div><article class="verify-cta">
<span>RUN IT YOURSELF · two labs, under 10 minutes each</span><div class="verify-cta-grid">
<a href={`${base}hands-on/starter/`} class="verify-card"
><strong>Path A · verification lab</strong><p>
Fill the four-row comparison strip on the starter. Run A naively, Run B with <code
>$gate-discipline</code
> and <code>$webapp-testing</code>.
</p><small>Open the starter </small><small class="verify-card-source"
>Clone <span>git.marcospaulo.dev.br/.../src/branch/pages/hands-on/starter</span
></small
></a
><a href={`${base}hands-on/rules/`} class="verify-card"
><strong>Path B · rules lab</strong><p>
Toggle every rule off, run the prompt. Toggle every rule on, run it again. Compare
diff size, gate invocations, and the names of checks the agent names back.
</p><small
><span data-language-content="en">Open the rules lab </span><span
data-language-content="pt"
hidden>Open the rules lab </span
></small
><small class="verify-card-source"
>Clone <span>git.marcospaulo.dev.br/.../src/branch/pages/hands-on/rules</span
></small
></a
>
</div>
</article>
</section>
<section class="sources">
<div class="section-label">
@@ -1064,6 +1397,22 @@ const base = import.meta.env.BASE_URL;
>
</p>
</section>
<!-- Legacy full-guide/index.html closes with this section after `.sources`.
Task 15d dropped it, and nothing caught that: verify.mjs reads the
legacy file, so its chapter-route assertion kept passing. It has no
`translations.pt` entry, so it is English-only on the live site too. -->
<section class="chapter-route">
<div class="section-label">
<span>Navigate by idea</span><span>short chapters / one system</span>
</div>
<p>
Prefer a focused chapter? Start with the <a href={`${base}summary/`}>route map</a>, then
jump directly to <a href={`${base}models/`}>models</a>, <a href={`${base}agents/`}
>agents and worktrees</a
>, <a href={`${base}skills/`}>skill creation</a>, <a href={`${base}rules/`}>rules</a>, or
the <a href={`${base}skills-review/`}>skills review desk</a>.
</p>
</section>
</main>
<GuideSelector rootSelector="#full-guide" data={selectorData} />
</BaseLayout>