ganbatte-os 0.2.34 → 0.2.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gos/agents/profiles/ganbatte-os-master.md +48 -8
- package/.gos/libraries/frontend/state-persistence-patterns.md +300 -0
- package/.gos/playbooks/plan-creation-playbook.md +34 -17
- package/.gos/scripts/integrations/check-plan.js +120 -0
- package/.gos/scripts/integrations/migrate-task-status.js +122 -0
- package/.gos/scripts/integrations/setup-ide-adapters.js +80 -66
- package/.gos/skills/execute-plan/SKILL.md +91 -20
- package/.gos/skills/plan-blueprint/SKILL.md +71 -6
- package/.gos/skills/plan-to-tasks/SKILL.md +13 -29
- package/.gos/skills/progress-tracker/SKILL.md +23 -3
- package/.gos/skills/registry.json +2 -1
- package/.gos/skills/validate-plan/SKILL.md +133 -0
- package/.gos/templates/planTemplate.md +50 -6
- package/.gos/templates/taskTemplate.md +6 -1
- package/CLAUDE.md +7 -4
- package/package.json +1 -1
|
@@ -99,6 +99,7 @@ comprehension_gate:
|
|
|
99
99
|
step_0_58_knowledge: "On *plan: index <PROJETO>/docs/regras-de-negocio/ and <PROJETO>/docs/postman/ (when present). Register inventory in progress.txt under '## Knowledge mapped — PLAN-NNN'. Ausência não bloqueia (apenas Storybook bloqueia)."
|
|
100
100
|
step_0_59_backend_gaps: "On *plan: detected backend gaps (endpoint não existe no Postman, RLS incompleto, migration ausente para o shape exigido) → criar task ClickUp via mcp__clickup__clickup_create_task, assignee Douglas Oliveira (112010775) salvo override ASSIGNEE no prompt. Título: '[Backend] PLAN-NNN: <gap>'. Registrar IDs em progress.txt e plan.md (## Backend pendings). Flag --skip-clickup desliga."
|
|
101
101
|
step_0_6_progress: "If progress.txt exists at the configured path: read it for active plan/task context (memória L1)."
|
|
102
|
+
step_1_2_interactions: "On *plan: detect if a tela tem table-clicável/drawer/modal/popup. Heurística: (a) Figma MCP frames com layer names contendo Drawer|Modal|Dialog|Popup|Sheet; (b) NOTAS menciona drawer/modal/popup/clickable row; (c) tela existente em dirs.app com componente equivalente (page.tsx que importa Drawer/Dialog). Se sim E INTERACOES ausente no input: BLOQUEAR plan-blueprint e disparar AskUserQuestion estruturado pedindo lista de interações com 3 exemplos pré-preenchidos (clickable row, submit assíncrono, filtro). Resposta vira entrada da Fase 1.4 do plan-blueprint (`## Interações & Estados`). Telas simples (form linear, lista read-only, página estática) NÃO acionam o bloqueio."
|
|
102
103
|
step_1_document: "State what exists (current state, patterns, constraints) in factual terms"
|
|
103
104
|
step_2_assess: "Determine which agent/skill/workflow is appropriate based on evidence, not assumption"
|
|
104
105
|
step_3_delegate: "Route with context summary so the target has full picture"
|
|
@@ -203,13 +204,30 @@ routing_matrix:
|
|
|
203
204
|
plan_creation:
|
|
204
205
|
triggers: [plan, plano, screen plan, tela, criar plano, blueprint, plano de tela]
|
|
205
206
|
target: skill:plan-blueprint
|
|
206
|
-
pre_action: Validate docs/stack.md exists; if not, dispatch stack-profiler first
|
|
207
|
+
pre_action: Validate docs/stack.md exists; if not, dispatch stack-profiler first. Run step_1_2_interactions — block + AskUserQuestion when tela tem table-clicável/drawer/modal/popup E INTERACOES ausente.
|
|
208
|
+
post_action: |
|
|
209
|
+
OBRIGATÓRIO em sequência ANTES de devolver "plano criado":
|
|
210
|
+
1. dispatch skill:plan-to-tasks apontando plan.md recém-criado (gera T-NN*.md no tasks/).
|
|
211
|
+
2. Bash: `node .gos/scripts/integrations/check-plan.js <plan-dir>` (gate determinístico).
|
|
212
|
+
3. Se exit != 0: regerar plan-to-tasks 1x e re-rodar check-plan. Se persistir, ABORTAR
|
|
213
|
+
e devolver a saída literal do check-plan ao usuário com instrução para migrate-task-status.js.
|
|
214
|
+
`*plan` NUNCA termina com tasks/ vazio. NUNCA termina sem exit 0 do check-plan.
|
|
207
215
|
notes: |
|
|
216
|
+
*plan é operação ATÔMICA: {plan.md + context.md + tasks/T-NN.md (TODAS) + progress.txt}.
|
|
217
|
+
Plano sem tasks = falha — plan-blueprint deve invocar plan-to-tasks E rodar o gate
|
|
218
|
+
determinístico `node .gos/scripts/integrations/check-plan.js <plan-dir>` como ÚLTIMA
|
|
219
|
+
ação. Exit != 0 bloqueia o "plano criado" — sem barreira no LLM (script roda fora).
|
|
220
|
+
Bug histórico: tasks geradas sem frontmatter ficavam travadas em pendente mesmo após
|
|
221
|
+
*execute-plan rodar código (PLAN-006). Planos preexistentes: rodar migrate-task-status.js.
|
|
222
|
+
|
|
208
223
|
1 tela = 1 plano. OBJETIVO obrigatório no prompt:
|
|
209
224
|
- implantacao → criar do zero (fluxo padrão)
|
|
210
225
|
- correcao → cirúrgico, diff vs Storybook, 1 task por componente
|
|
211
226
|
- refactor → implica --allow-arch-change + ADR
|
|
212
227
|
Auto-resolve PROJETO/WORK_BRANCH/BUSINESS_RULES/POSTMAN no comprehension gate.
|
|
228
|
+
INTERACOES obrigatório quando tela tem table-clicável/drawer/modal/popup — gos-master bloqueia
|
|
229
|
+
e abre AskUserQuestion se ausente. Plano captura comportamentos (Fase 1.4 do plan-blueprint)
|
|
230
|
+
e page-level overrides (Figma da página > Storybook canônico em conflito visual).
|
|
213
231
|
Backend gaps → tasks ClickUp automáticas pro Douglas (--skip-clickup desliga).
|
|
214
232
|
|
|
215
233
|
progress_tracking:
|
|
@@ -220,11 +238,28 @@ routing_matrix:
|
|
|
220
238
|
execute_plan:
|
|
221
239
|
triggers: [execute, executar plano, run plan, "*execute-plan", execute plan, executar PLAN]
|
|
222
240
|
target: skill:execute-plan
|
|
223
|
-
pre_action: Validate PLAN-NNN-<slug>/plan.md exists; load stack.md; index dirs.storybook stories
|
|
241
|
+
pre_action: Validate PLAN-NNN-<slug>/plan.md exists; load stack.md; index dirs.storybook stories. Verificar que cada T-NN.md tem frontmatter com `status:` — task malformada ABORTA com instrução para rodar migrate-task-status.js.
|
|
224
242
|
notes: |
|
|
225
243
|
Comando primário do ambiente Codex IDE Extension.
|
|
226
|
-
Ciclo Opus(plan) → Codex(execute). Roda task-a-task com state machine
|
|
227
|
-
e visual gate obrigatório
|
|
244
|
+
Ciclo Opus(plan) → Codex(execute) → Opus(validate). Roda task-a-task com state machine
|
|
245
|
+
e visual gate obrigatório (5 dimensões: anatomia, tokens, variants, densidade, comportamentos)
|
|
246
|
+
contra Storybook canônico antes de validacao. Pre-flight smoke compara screenshot da página
|
|
247
|
+
vs Figma frame antes da T-01 (gera tasks T-000-XX para gaps grandes).
|
|
248
|
+
Non-blocking em backend gaps: tasks com depends_on_backend não-resolvido viram
|
|
249
|
+
bloqueada-backend (ClickUp aberto pro Douglas), demais seguem.
|
|
250
|
+
Transições de status são VINCULANTES com pós-condição: cada task DEVE sair de pendente
|
|
251
|
+
antes da próxima — se o executor não chamar progress-tracker, abortar (bug PLAN-006).
|
|
252
|
+
|
|
253
|
+
validate_plan:
|
|
254
|
+
triggers: [validate, validar plano, "*validate-plan", validate plan, revisar plano, plano implementado]
|
|
255
|
+
target: skill:validate-plan
|
|
256
|
+
pre_action: Validate PLAN-NNN-<slug>/plan.md exists; load progress.txt
|
|
257
|
+
notes: |
|
|
258
|
+
Turn pós-execute. Para cada task em validacao, re-roda visual gate curto
|
|
259
|
+
(anatomia + tokens, sem refazer Figma MCP), confronta diff staged vs
|
|
260
|
+
Componentes mapeados, confere checklist do plano e da task.
|
|
261
|
+
Auto-marca concluido o que passa. Fecha plano quando todas tasks fecham
|
|
262
|
+
E backend pendings ClickUp estão concluídas. NÃO dá push.
|
|
228
263
|
|
|
229
264
|
product_decisions:
|
|
230
265
|
triggers: [PRD, requirements, product decision, scope, feature priority]
|
|
@@ -313,8 +348,11 @@ commands:
|
|
|
313
348
|
args: "[init|show|set <plan>|status <task> <novo-status>|compact|read]"
|
|
314
349
|
description: Gerencia progress.txt (memória L1) e state machine de status
|
|
315
350
|
- name: execute-plan
|
|
316
|
-
args: "<PLAN-NNN-slug> [--task T-NNN-NN] [--skip-visual-gate]"
|
|
317
|
-
description: Executa plano task-a-task com visual gate obrigatório
|
|
351
|
+
args: "<PLAN-NNN-slug> [--task T-NNN-NN] [--skip-visual-gate] [--skip-clickup]"
|
|
352
|
+
description: Executa plano task-a-task com visual gate obrigatório, non-blocking em backend gaps (Codex IDE)
|
|
353
|
+
- name: validate-plan
|
|
354
|
+
args: "<PLAN-NNN-slug> [NOTAS=...]"
|
|
355
|
+
description: Valida plano pós-execute; auto-marca concluido tasks que passam em checklist + visual gate curto + diff (Opus revisor)
|
|
318
356
|
|
|
319
357
|
# Quality
|
|
320
358
|
- name: check
|
|
@@ -351,7 +389,7 @@ available_squads:
|
|
|
351
389
|
- name: git-operations
|
|
352
390
|
purpose: SSH setup, quality gate, safe commit+push
|
|
353
391
|
|
|
354
|
-
# ─── AVAILABLE SKILLS (
|
|
392
|
+
# ─── AVAILABLE SKILLS (20) ────────────────────────────────────
|
|
355
393
|
available_skills:
|
|
356
394
|
- design-to-code
|
|
357
395
|
- figma-implement-design
|
|
@@ -372,6 +410,7 @@ available_skills:
|
|
|
372
410
|
- plan-blueprint
|
|
373
411
|
- progress-tracker
|
|
374
412
|
- execute-plan
|
|
413
|
+
- validate-plan
|
|
375
414
|
|
|
376
415
|
# ─── PLAYBOOKS ────────────────────────────────────────────────
|
|
377
416
|
available_playbooks:
|
|
@@ -474,7 +513,8 @@ security:
|
|
|
474
513
|
|
|
475
514
|
- `*stack [refresh|show|drift]` - Mantém docs/stack.md
|
|
476
515
|
- `*plan <tela|figma-url|descrição>` - Cria plano por tela (Opus, planejamento)
|
|
477
|
-
- `*execute-plan <PLAN-NNN-slug>` - Executa plano com visual gate (Codex IDE, execução)
|
|
516
|
+
- `*execute-plan <PLAN-NNN-slug>` - Executa plano com visual gate, non-blocking em backend gaps (Codex IDE, execução)
|
|
517
|
+
- `*validate-plan <PLAN-NNN-slug>` - Valida plano pós-execute; auto-marca concluido (Opus, revisor)
|
|
478
518
|
- `*progress [show|set|status|compact]` - Gerencia progress.txt (L1)
|
|
479
519
|
|
|
480
520
|
**Framework:**
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# State Persistence Patterns (FOUC Zero)
|
|
2
|
+
|
|
3
|
+
Reference library for persisting UI state across reloads in SSR/Edge frameworks (Next.js App Router, Remix, SvelteKit) **without** flash of incorrect content (FOUC) or hydration mismatch.
|
|
4
|
+
|
|
5
|
+
This library is the positive counterpart to `content/ai-writing-patterns.md`: instead of cataloging anti-patterns to avoid, it codifies **patterns to apply** for any UI state that survives reload.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When this applies
|
|
10
|
+
|
|
11
|
+
State is "persisted UI state" if **all** of the following are true:
|
|
12
|
+
|
|
13
|
+
1. It survives page reloads (cookie, localStorage, URL, server session).
|
|
14
|
+
2. It affects the **first paint** of the layout (sidebar collapsed/expanded, theme, density, locale, RTL/LTR, font-size preset).
|
|
15
|
+
3. It has a default value used when the persisted value is missing.
|
|
16
|
+
|
|
17
|
+
If state is client-only and **does not** affect the first paint (form drafts, in-page panels not visible at load, ephemeral preferences) — these patterns are not required; localStorage + `useEffect` is fine.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The core problem: client-only persistence creates FOUC
|
|
22
|
+
|
|
23
|
+
A naive implementation reads the persisted value on the client *after* hydration:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// ANTI-PATTERN — produces visible flash
|
|
27
|
+
function SidebarProvider({ defaultOpen = true, children }) {
|
|
28
|
+
const [open, setOpen] = useState(defaultOpen)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const saved = document.cookie.match(/sidebar_state=(\w+)/)?.[1]
|
|
32
|
+
if (saved === "false") setOpen(false) // flash: SSR rendered open, this closes it
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
return <Context.Provider value={{ open }}>{children}</Context.Provider>
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Sequence that produces the flash:
|
|
40
|
+
|
|
41
|
+
1. **SSR**: HTML is rendered with `defaultOpen=true` (server has no access to client state).
|
|
42
|
+
2. **First paint**: browser shows sidebar **open**.
|
|
43
|
+
3. **Hydration**: React mounts with `open=true`.
|
|
44
|
+
4. **`useEffect` fires**: reads cookie `false` → `setOpen(false)` → re-render → sidebar collapses.
|
|
45
|
+
|
|
46
|
+
The flash is 16–200ms depending on network/CPU. On slow connections it is painful UX.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Pattern A — Server-read cookie + `defaultX` injection (canonical)
|
|
51
|
+
|
|
52
|
+
**When to use**: any state where the persistence medium is a **cookie** and the framework supports reading cookies in Server Components.
|
|
53
|
+
|
|
54
|
+
**Next.js App Router (canonical)**:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app-shell.tsx — Server Component
|
|
58
|
+
import { cookies } from "next/headers"
|
|
59
|
+
import { SidebarProvider } from "@/components/ui/sidebar"
|
|
60
|
+
|
|
61
|
+
export async function AppShell({ children }: { children: React.ReactNode }) {
|
|
62
|
+
const cookieStore = await cookies() // Next 15+ async; Next 14 sync
|
|
63
|
+
const saved = cookieStore.get("sidebar_state")?.value
|
|
64
|
+
const defaultOpen = saved === undefined ? true : saved === "true"
|
|
65
|
+
|
|
66
|
+
return <SidebarProvider defaultOpen={defaultOpen}>{children}</SidebarProvider>
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// sidebar.tsx — Client Component
|
|
72
|
+
"use client"
|
|
73
|
+
|
|
74
|
+
export function SidebarProvider({ defaultOpen = true, children }) {
|
|
75
|
+
const [open, _setOpen] = useState(defaultOpen) // hydrates with correct value
|
|
76
|
+
|
|
77
|
+
const setOpen = useCallback((next: boolean) => {
|
|
78
|
+
_setOpen(next)
|
|
79
|
+
document.cookie = `sidebar_state=${next}; path=/; max-age=${60 * 60 * 24 * 7}`
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
82
|
+
return <Context.Provider value={{ open, setOpen }}>{children}</Context.Provider>
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Why this works**:
|
|
87
|
+
- Server reads cookie → SSR HTML reflects the persisted state.
|
|
88
|
+
- First paint already correct.
|
|
89
|
+
- Hydration uses `defaultOpen` from server → no client-side correction needed.
|
|
90
|
+
- `useEffect` is **not used for initial state** — only `setOpen` writes the cookie when the user toggles.
|
|
91
|
+
|
|
92
|
+
**Cost**: `cookies()` forces dynamic rendering of the page that uses it. Acceptable for authenticated dashboards (already dynamic). For static pages, use Pattern C.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Pattern B — `headers()` for derived state (locale, theme by system pref proxy)
|
|
97
|
+
|
|
98
|
+
When the state can be derived from a request header (e.g., `Accept-Language`, `Sec-CH-Prefers-Color-Scheme`), read it server-side and inject as default:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { headers } from "next/headers"
|
|
102
|
+
|
|
103
|
+
export async function ThemeShell({ children }) {
|
|
104
|
+
const h = await headers()
|
|
105
|
+
const prefersDark = h.get("sec-ch-prefers-color-scheme") === "dark"
|
|
106
|
+
return <ThemeProvider defaultTheme={prefersDark ? "dark" : "light"}>{children}</ThemeProvider>
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Use this **in addition to** Pattern A: server reads cookie first; if missing, derives from headers; client toggles persist back to cookie.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Pattern C — Inline blocking script for static pages (last resort)
|
|
115
|
+
|
|
116
|
+
When the page **must** stay static (cannot opt into dynamic rendering) but still needs FOUC-free persisted state, use a small synchronous script in `<head>` that sets a class/attribute on `<html>` **before** the first paint:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// app/layout.tsx
|
|
120
|
+
import Script from "next/script"
|
|
121
|
+
|
|
122
|
+
export default function RootLayout({ children }) {
|
|
123
|
+
return (
|
|
124
|
+
<html lang="pt-BR" suppressHydrationWarning>
|
|
125
|
+
<head>
|
|
126
|
+
<Script id="set-sidebar-state" strategy="beforeInteractive">
|
|
127
|
+
{`(function(){try{var m=document.cookie.match(/sidebar_state=(\\w+)/);
|
|
128
|
+
if(m&&m[1]==="false"){document.documentElement.dataset.sidebar="collapsed"}}catch(e){}})()`}
|
|
129
|
+
</Script>
|
|
130
|
+
</head>
|
|
131
|
+
<body>{children}</body>
|
|
132
|
+
</html>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then CSS reads the attribute:
|
|
138
|
+
|
|
139
|
+
```css
|
|
140
|
+
[data-sidebar="collapsed"] aside { width: 56px; }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Caveats**:
|
|
144
|
+
- Adds blocking JS to critical path (~1KB).
|
|
145
|
+
- CSP must allow the inline script (or extract to external file with hash).
|
|
146
|
+
- Only use when Pattern A is impossible (truly static pages).
|
|
147
|
+
|
|
148
|
+
This is what `next-themes` does for theme persistence on static pages.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Pattern D — URL state (no FOUC by definition)
|
|
153
|
+
|
|
154
|
+
For state that should be **shareable** (filters, tabs, pagination), persist in the URL search params. The server reads `searchParams` on render, so first paint is always correct:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// page.tsx — Server Component
|
|
158
|
+
export default async function Page({ searchParams }) {
|
|
159
|
+
const params = await searchParams
|
|
160
|
+
const view = params.view ?? "grid"
|
|
161
|
+
return <Layout defaultView={view}>...</Layout>
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This trades persistence-across-sessions (cookies persist; URL is per-tab) for shareability and bookmarkability. Use both: URL for view state, cookie for chrome (sidebar/theme).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Decision tree
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Does the state affect first paint?
|
|
173
|
+
├── No → localStorage + useEffect is fine.
|
|
174
|
+
└── Yes → Is it shareable / bookmarkable?
|
|
175
|
+
├── Yes → Pattern D (URL search params).
|
|
176
|
+
└── No → Is the route already dynamic?
|
|
177
|
+
├── Yes → Pattern A (cookies() in Server Component).
|
|
178
|
+
└── No → Can the route opt into dynamic rendering?
|
|
179
|
+
├── Yes → Pattern A; route becomes dynamic.
|
|
180
|
+
└── No → Pattern C (blocking script).
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Anti-patterns (catalog)
|
|
186
|
+
|
|
187
|
+
### A1. `useEffect` to read persisted state and `setState`
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// BAD
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const saved = localStorage.getItem("theme")
|
|
193
|
+
if (saved) setTheme(saved)
|
|
194
|
+
}, [])
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Why bad**: SSR renders default; client corrects after hydration → flash.
|
|
198
|
+
|
|
199
|
+
**Fix**: Pattern A or C.
|
|
200
|
+
|
|
201
|
+
### A2. Conditional render that hides until hydrated
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
// BAD
|
|
205
|
+
const [hydrated, setHydrated] = useState(false)
|
|
206
|
+
useEffect(() => setHydrated(true), [])
|
|
207
|
+
if (!hydrated) return null // layout shift on hydration
|
|
208
|
+
return <Sidebar open={open} />
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Why bad**: layout shift (CLS hit), accessibility regression (sidebar invisible to screen readers initially).
|
|
212
|
+
|
|
213
|
+
**Fix**: Pattern A or C.
|
|
214
|
+
|
|
215
|
+
### A3. `suppressHydrationWarning` to mask the mismatch
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
// BAD
|
|
219
|
+
<aside suppressHydrationWarning data-state={open ? "open" : "closed"}>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Why bad**: hides the symptom, not the cause. The actual visual flash still happens.
|
|
223
|
+
|
|
224
|
+
**Fix**: solve the root cause with Pattern A. Use `suppressHydrationWarning` only on `<html>` for theme (where the script in Pattern C legitimately mutates the attribute before React hydrates).
|
|
225
|
+
|
|
226
|
+
### A4. Cookie set in client without server read
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
// BAD — server cannot read what only the client wrote
|
|
230
|
+
function setOpen(next) {
|
|
231
|
+
document.cookie = `sidebar_state=${next}`
|
|
232
|
+
setOpenState(next)
|
|
233
|
+
}
|
|
234
|
+
// And the server never reads "sidebar_state" → flash on every reload.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Why bad**: persistence works (cookie is sent on next request), but the server doesn't use it — the loop is incomplete.
|
|
238
|
+
|
|
239
|
+
**Fix**: complete the loop — server reads in Pattern A.
|
|
240
|
+
|
|
241
|
+
### A5. localStorage for layout-affecting state
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
// BAD for state affecting first paint
|
|
245
|
+
localStorage.setItem("sidebar_state", "false")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Why bad**: localStorage is **client-only** by definition; no server-side reading possible. Inevitable flash.
|
|
249
|
+
|
|
250
|
+
**Fix**: migrate to cookie (Pattern A) or accept blocking script (Pattern C). localStorage is fine for client-only state that doesn't affect first paint.
|
|
251
|
+
|
|
252
|
+
### A6. Default state assumed identical for all users
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
// BAD when users have different defaults (locale, timezone, RTL)
|
|
256
|
+
<App defaultLocale="en" />
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Why bad**: ignores `Accept-Language`; first paint is "wrong" for half the users until client correction.
|
|
260
|
+
|
|
261
|
+
**Fix**: Pattern B (read header) + Pattern A (override with user-saved cookie).
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Checklist for any persisted UI state
|
|
266
|
+
|
|
267
|
+
Before merging a feature that persists state:
|
|
268
|
+
|
|
269
|
+
- [ ] Does the state affect first paint? If yes:
|
|
270
|
+
- [ ] Is the persistence medium readable on the server (cookie, header)? If no, migrate it.
|
|
271
|
+
- [ ] Does the Server Component / loader pass the persisted value as `defaultX` to the Client Component? If no, add it.
|
|
272
|
+
- [ ] Is there a `useEffect` reading the persisted value to update state? If yes, **remove it** (Pattern A makes it redundant).
|
|
273
|
+
- [ ] Does the toggle UI write to the same persistence medium the server reads? (Cookie path/expiry must match.)
|
|
274
|
+
- [ ] Tested on Slow 3G: zero flash on reload?
|
|
275
|
+
- [ ] Tested in incognito (no cookie): falls back to default cleanly?
|
|
276
|
+
- [ ] Tested on mobile (different state model often required).
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Real incidents this library prevents
|
|
281
|
+
|
|
282
|
+
- **Sidebar flash open→closed on reload** (Fractus 2026-05-04, T-51): cookie persisted but read in `useEffect`; fix moved read to `cookies()` in Server Component.
|
|
283
|
+
- **Theme flash light→dark on reload** (common pre-`next-themes`): solved by Pattern C blocking script. `next-themes` automates it.
|
|
284
|
+
- **Wrong locale flash for non-English users**: solved by Pattern B reading `Accept-Language`.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## References
|
|
289
|
+
|
|
290
|
+
- Next.js docs: [`cookies()`](https://nextjs.org/docs/app/api-reference/functions/cookies), [`headers()`](https://nextjs.org/docs/app/api-reference/functions/headers).
|
|
291
|
+
- shadcn/ui sidebar: [persistence pattern](https://ui.shadcn.com/docs/components/sidebar) — uses Pattern A by default.
|
|
292
|
+
- `next-themes`: source for Pattern C reference implementation.
|
|
293
|
+
- React docs: [hydration mismatches](https://react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Cross-references in this framework
|
|
298
|
+
|
|
299
|
+
- Anti-patterns of writing/content: `.gos/libraries/content/ai-writing-patterns.md` (analogous catalog format).
|
|
300
|
+
- (Add `.gos/libraries/frontend/nextjs-patterns.md` and `react-patterns.md` here when ported.)
|
|
@@ -41,26 +41,29 @@ Se houver drift, decidir entre:
|
|
|
41
41
|
```
|
|
42
42
|
*plan <tela>
|
|
43
43
|
|
|
44
|
-
OBJETIVO
|
|
45
|
-
FIGMA
|
|
46
|
-
FIGMA+
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
OBJETIVO = implantacao | correcao | refactor # obrigatório
|
|
45
|
+
FIGMA = <url-frame>
|
|
46
|
+
FIGMA+ = [<url-comp>, ...] # opcional
|
|
47
|
+
INTERACOES = """<lista estruturada — obrigatório quando tela tem table-clicável/drawer/modal/popup>"""
|
|
48
|
+
NOTAS = """<prosa livre>""" # opcional
|
|
49
|
+
ASSIGNEE = <user-id> # opcional, default 112010775 (Douglas)
|
|
49
50
|
```
|
|
50
51
|
|
|
51
52
|
`gos-master` resolve no comprehension gate (não pedir ao usuário):
|
|
52
53
|
- `PROJETO` (cwd, ou `~/.claude/.gos-state/last-project.json`)
|
|
53
54
|
- `WORK_BRANCH` (`dev` para app, `feat/storybook` quando em Storybook)
|
|
54
55
|
- Indexação de `<PROJETO>/docs/regras-de-negocio/` e `docs/postman/` (registrada em `progress.txt`)
|
|
56
|
+
- **Step 1.2 — Coleta de comportamentos**: detecta se tela tem table-clicável/drawer/modal/popup. Sim E `INTERACOES` ausente → bloqueia + dispara `AskUserQuestion` estruturado pedindo as interações (3 exemplos pré-preenchidos). Telas simples (form linear, lista read-only) não acionam.
|
|
55
57
|
|
|
56
58
|
`plan-blueprint` executa:
|
|
57
59
|
1. Fase 1 — Mapeamento Visual & Componentização
|
|
60
|
+
1.4 Comportamentos & Overrides — `## Interações & Estados` + `## Page-level overrides` no plano
|
|
58
61
|
2. Fase 2 — Aderência à Stack (sem redefinir arquitetura)
|
|
59
62
|
2.5 Fase 2.5 — Backend gaps → criar tasks ClickUp pro Douglas (`--skip-clickup` desliga)
|
|
60
|
-
3. Fase 3 — Plano de Execução
|
|
63
|
+
3. Fase 3 — Plano de Execução (gera tasks com `interaction_target` e `override_target` para cobrir comportamentos e overrides)
|
|
61
64
|
|
|
62
65
|
Saídas:
|
|
63
|
-
- `<dirs.planos>/PLAN-NNN-<slug>/plan.md` (com seções `## Backend pendings
|
|
66
|
+
- `<dirs.planos>/PLAN-NNN-<slug>/plan.md` (com seções `## Backend pendings`, `## Knowledge mapped`, `## Interações & Estados`, `## Page-level overrides`)
|
|
64
67
|
- `<dirs.planos>/PLAN-NNN-<slug>/context.md`
|
|
65
68
|
- `<dirs.planos>/PLAN-NNN-<slug>/tasks/T-NNN-NN-*.md`
|
|
66
69
|
- `progress.txt` atualizado com plano ativo + inventário de knowledge + backend pendings
|
|
@@ -80,7 +83,10 @@ Se aprovado, prosseguir. Caso contrário: ajustar manualmente ou rerodar `*plan`
|
|
|
80
83
|
*execute-plan PLAN-NNN-<slug>
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
A skill `execute-plan`
|
|
86
|
+
A skill `execute-plan` faz dois pre-flights antes da T-01:
|
|
87
|
+
|
|
88
|
+
1. **Stories disponíveis**: resolve `dirs.storybook`, indexa `.stories.tsx`, confronta cada componente da tabela "Componentes mapeados". Componente sem story → bloqueia e propõe task de criação ANTES das tasks de implementação.
|
|
89
|
+
2. **Visual smoke** (quando Storybook story-da-página OU Playwright MCP disponível): renderiza a página com seed e compara screenshot vs frame Figma em 3 dimensões básicas (seções presentes, layout grosseiro, cores primárias). Gaps grandes viram tasks `T-000-XX` com `priority: P0` antes do loop. Sem Storybook full-page nem Playwright MCP: skip com warning. Esse pre-flight evita o padrão PLAN-005 (gaps grandes descobertos via 26 rodadas de feedback no fim).
|
|
84
90
|
|
|
85
91
|
> Ambiente recomendado: **Codex IDE Extension** (executor). O `*plan` da etapa 2 roda em Opus 4.7 (planejador). Adapter Codex é gerado por `npm run sync:ides`.
|
|
86
92
|
|
|
@@ -91,28 +97,38 @@ A skill `execute-plan` resolve `dirs.storybook`, indexa `.stories.tsx` disponív
|
|
|
91
97
|
1. `*progress status T-NNN-NN em-andamento` (state machine).
|
|
92
98
|
2. Despacha agent (`labels: [agent:<slug>]`, default `dev`) para implementar.
|
|
93
99
|
3. **Visual gate** antes de marcar `validacao`:
|
|
94
|
-
- Compara cada componente alterado com `<Componente>.stories.tsx` em
|
|
100
|
+
- Compara cada componente alterado com `<Componente>.stories.tsx` em 5 dimensões (anatomia, tokens, variants, densidade, comportamentos). Tokens divergentes registrados em `## Page-level overrides` do plano não falham — gate confirma aplicação do override.
|
|
101
|
+
- Comportamentos: cada `interaction_target:` declarado na task tem handler/estado observável no diff; cada `override_target:` tem classes/props da decisão (a/b/c) presentes.
|
|
95
102
|
- Cruza JSX da tela com Figma MCP (árvore vs hierarquia).
|
|
96
103
|
- Falha → task volta a `em-andamento` com diff em `tasks/T-NNN-NN.notes.md`.
|
|
97
104
|
4. Sucesso → `*progress status T-NNN-NN validacao`.
|
|
98
105
|
|
|
99
106
|
Para rodar uma task específica fora do loop: `*execute-plan PLAN-NNN-<slug> --task T-NNN-NN`.
|
|
100
107
|
|
|
101
|
-
### 5. Validação
|
|
108
|
+
### 4.5. Validação pós-execute
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
Após `*execute-plan` retornar (com tasks em `validacao` e/ou `bloqueada-backend`), rodar:
|
|
104
111
|
|
|
105
112
|
```
|
|
106
|
-
*
|
|
113
|
+
*validate-plan PLAN-NNN-<slug>
|
|
114
|
+
|
|
115
|
+
NOTAS = """<opcional — desvios conhecidos, contexto de QA>"""
|
|
107
116
|
```
|
|
108
117
|
|
|
109
|
-
|
|
118
|
+
Skill `validate-plan` (Opus, revisor):
|
|
119
|
+
- Para cada task em `validacao`: re-roda visual gate curto (anatomia + tokens + comportamentos), confronta `git diff --staged` vs Componentes mapeados, confere checklist da task e `interaction_target`/`override_target`. Tudo bate → auto-marca `concluido`. Falha → mantém `validacao` com nota.
|
|
120
|
+
- Para o plano: cobertura de comportamento (`## Interações & Estados`) + cobertura de overrides (`## Page-level overrides`) + checklist do plano + backend pendings ClickUp todos `concluido` → marca `validated_at:` no plan.md + `*progress status PLAN-NNN-<slug> concluido`. Senão → mantém em `validacao`.
|
|
121
|
+
- Tasks `bloqueada-backend` ficam intocadas — backend tem que fechar primeiro.
|
|
122
|
+
|
|
123
|
+
### 5. Push manual e fechamento
|
|
124
|
+
|
|
125
|
+
`validate-plan` NÃO dá push. Quando o plano fecha 100%:
|
|
110
126
|
|
|
111
127
|
```
|
|
112
|
-
|
|
128
|
+
git push
|
|
113
129
|
```
|
|
114
130
|
|
|
115
|
-
|
|
131
|
+
Push é ato consciente do humano. Tasks `bloqueada-backend` aguardam ClickUp fechar — quando isso acontece, rodar `*progress status T-NNN-NN em-andamento` (state machine valida ClickUp via MCP) e voltar pra etapa 4.
|
|
116
132
|
|
|
117
133
|
### 6. Commit & resumo
|
|
118
134
|
|
|
@@ -140,6 +156,7 @@ Validação humana + QA. Se aprovado:
|
|
|
140
156
|
- `stack-profiler` — produz/mantém `stack.md`
|
|
141
157
|
- `plan-blueprint` — cria plano por tela (Opus, planejamento)
|
|
142
158
|
- `plan-to-tasks` — decompõe plano em tasks (chamada automaticamente)
|
|
143
|
-
- `execute-plan` — executa plano com visual gate (Codex IDE, execução)
|
|
144
|
-
- `
|
|
159
|
+
- `execute-plan` — executa plano com visual gate, non-blocking em backend gaps (Codex IDE, execução)
|
|
160
|
+
- `validate-plan` — valida plano pós-execute, auto-marca concluido (Opus, revisor)
|
|
161
|
+
- `progress-tracker` — gerencia `progress.txt` + state machine (incluindo `bloqueada-backend`)
|
|
145
162
|
- `clickup` — sync opcional para tracking externo (não obrigatório)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-plan.js — barreira deterministica de criacao de plano.
|
|
4
|
+
*
|
|
5
|
+
* Roda como ULTIMA acao obrigatoria do *plan (apos plan-blueprint +
|
|
6
|
+
* plan-to-tasks). Valida que o plano e usavel pelo *execute-plan e
|
|
7
|
+
* *validate-plan. Falha = *plan nao terminou — usuario ve erro, nao
|
|
8
|
+
* "plano criado" ilusorio.
|
|
9
|
+
*
|
|
10
|
+
* Uso:
|
|
11
|
+
* node check-plan.js <plan-dir>
|
|
12
|
+
*
|
|
13
|
+
* Verificacoes:
|
|
14
|
+
* 1. plan.md existe e tem frontmatter YAML.
|
|
15
|
+
* 2. context.md existe.
|
|
16
|
+
* 3. tasks/ existe e contem >= 1 T-NN*.md.
|
|
17
|
+
* 4. Cada T-NN*.md: head -1 == "---" E frontmatter contem `status: pendente`
|
|
18
|
+
* E `id:`, `plan_id:`, `seq:`, `title:`.
|
|
19
|
+
* 5. Nenhum T-NN*.md tem secao `## Status` no body (formato bugado legado).
|
|
20
|
+
*
|
|
21
|
+
* Exit code:
|
|
22
|
+
* 0 = plano valido, usavel pelo pipeline
|
|
23
|
+
* 1 = falha — mensagem aponta exatamente o que esta errado
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const planDir = process.argv[2];
|
|
30
|
+
|
|
31
|
+
if (!planDir) {
|
|
32
|
+
console.error('uso: check-plan.js <plan-dir>');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(planDir)) {
|
|
37
|
+
console.error(`[check-plan] FALHA: plan-dir nao existe: ${planDir}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const errors = [];
|
|
42
|
+
const warnings = [];
|
|
43
|
+
|
|
44
|
+
const planFile = path.join(planDir, 'plan.md');
|
|
45
|
+
const contextFile = path.join(planDir, 'context.md');
|
|
46
|
+
const tasksDir = path.join(planDir, 'tasks');
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(planFile)) {
|
|
49
|
+
errors.push(`plan.md ausente em ${planDir}`);
|
|
50
|
+
} else {
|
|
51
|
+
const plan = fs.readFileSync(planFile, 'utf8');
|
|
52
|
+
if (!plan.startsWith('---\n')) {
|
|
53
|
+
errors.push(`plan.md sem frontmatter YAML (head -1 != "---")`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(contextFile)) {
|
|
58
|
+
warnings.push(`context.md ausente — recomendado, nao bloqueia`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(tasksDir)) {
|
|
62
|
+
errors.push(`tasks/ ausente em ${planDir} — *plan deve invocar plan-to-tasks`);
|
|
63
|
+
} else {
|
|
64
|
+
const taskFiles = fs.readdirSync(tasksDir)
|
|
65
|
+
.filter((f) => /^T-\d+.*\.md$/.test(f))
|
|
66
|
+
.map((f) => path.join(tasksDir, f));
|
|
67
|
+
|
|
68
|
+
if (taskFiles.length === 0) {
|
|
69
|
+
errors.push(`tasks/ vazio — nenhum T-NN*.md encontrado`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const file of taskFiles) {
|
|
73
|
+
const rel = path.relative(planDir, file);
|
|
74
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
75
|
+
|
|
76
|
+
if (!raw.startsWith('---\n')) {
|
|
77
|
+
errors.push(`${rel}: sem frontmatter YAML (head -1 != "---")`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fmEnd = raw.indexOf('\n---', 4);
|
|
82
|
+
if (fmEnd === -1) {
|
|
83
|
+
errors.push(`${rel}: frontmatter aberto sem fechar ("---" final ausente)`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const frontmatter = raw.slice(4, fmEnd);
|
|
87
|
+
const body = raw.slice(fmEnd + 4);
|
|
88
|
+
|
|
89
|
+
const required = ['id', 'plan_id', 'seq', 'title', 'status'];
|
|
90
|
+
for (const key of required) {
|
|
91
|
+
if (!new RegExp(`^${key}:\\s*\\S`, 'm').test(frontmatter)) {
|
|
92
|
+
errors.push(`${rel}: frontmatter sem campo \`${key}:\``);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!/^status:\s*pendente\s*$/m.test(frontmatter)) {
|
|
97
|
+
errors.push(`${rel}: frontmatter status: deve ser exatamente "pendente" no plano recem-criado`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (/^## Status\s*$/m.test(body)) {
|
|
101
|
+
errors.push(`${rel}: contem secao "## Status" no body (formato legado bugado) — rodar migrate-task-status.js`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (warnings.length) {
|
|
107
|
+
for (const w of warnings) console.error(`[check-plan] aviso: ${w}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (errors.length) {
|
|
111
|
+
console.error(`\n[check-plan] FALHA — plano NAO usavel pelo *execute-plan:\n`);
|
|
112
|
+
for (const e of errors) console.error(` - ${e}`);
|
|
113
|
+
console.error(`\nproximo passo: regerar tasks via plan-to-tasks OU rodar migrate-task-status.js ${planDir}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const taskCount = fs.readdirSync(tasksDir)
|
|
118
|
+
.filter((f) => /^T-\d+.*\.md$/.test(f)).length;
|
|
119
|
+
console.log(`[check-plan] OK — ${planDir}: plan.md + context.md + ${taskCount} tasks (todas com frontmatter status: pendente).`);
|
|
120
|
+
process.exit(0);
|