product-runner 0.5.0

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.
Files changed (37) hide show
  1. package/README.md +165 -0
  2. package/common/agents/README.md +68 -0
  3. package/common/agents/agente-conceituacao.md +141 -0
  4. package/common/agents/agente-documentacao-funcional.md +107 -0
  5. package/common/agents/agente-gerador-spec.md +106 -0
  6. package/common/agents/agente-kickoff.md +121 -0
  7. package/common/agents/agente-prod-runner.md +107 -0
  8. package/common/agents/agente-review-code.md +97 -0
  9. package/common/agents/agente-review-llm.md +94 -0
  10. package/common/agents/agente-review-product.md +98 -0
  11. package/common/agents/agente-user-review.md +99 -0
  12. package/common/agents/protocolo-de-gates.md +51 -0
  13. package/common/claude-md.template.md +210 -0
  14. package/common/design-principles.md +229 -0
  15. package/common/lessons-learned.md +440 -0
  16. package/common/pipeline.md +143 -0
  17. package/common/spec-guide.md +327 -0
  18. package/common/specs/_open-issues.md +46 -0
  19. package/common/specs/_overview.md +75 -0
  20. package/dist/cli.js +187 -0
  21. package/dist/migrations.js +147 -0
  22. package/dist/scaffold.js +276 -0
  23. package/dist/update.js +400 -0
  24. package/migrations/0.3.0.md +54 -0
  25. package/migrations/0.4.0.md +76 -0
  26. package/migrations/0.5.0.md +55 -0
  27. package/migrations/README.md +68 -0
  28. package/package.json +41 -0
  29. package/profile-cli/README.md +54 -0
  30. package/profile-cli/claude-md.extension.md +102 -0
  31. package/profile-cli/code-patterns.md +363 -0
  32. package/profile-ssr/DESIGN-SYSTEM.md +795 -0
  33. package/profile-ssr/README.md +51 -0
  34. package/profile-ssr/api-patterns.md +70 -0
  35. package/profile-ssr/claude-md.extension.md +113 -0
  36. package/profile-ssr/code-patterns.md +175 -0
  37. package/profile-ssr/ui-patterns.md +97 -0
package/dist/update.js ADDED
@@ -0,0 +1,400 @@
1
+ import { mkdir, readFile, writeFile, access, rm, readdir } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { spawn } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { buildArtifacts, listFiles, sha256, writeManifest, templatesRoot, packageVersion, MANIFEST_FILENAME, } from "./scaffold.js";
6
+ import { discoverMigrations, migrationsInSpan, applyOps, globToRegExp, } from "./migrations.js";
7
+ /** Subpasta (dentro de docs/) onde vão os artefatos de handoff dos conflitos. */
8
+ export const HANDOFF_DIR = ".prod-runner-update";
9
+ /**
10
+ * Nome do manifesto ANTES do rename para `product-runner` (migration 0.5.0).
11
+ * Projetos scaffoldados em versões anteriores têm o manifesto com este nome; o
12
+ * `readManifest` cai pra ele pra ainda achar o cursor — e a migration 0.5.0
13
+ * renomeia o arquivo no disco. Literal de propósito (não derivar do novo nome).
14
+ */
15
+ const LEGACY_MANIFEST_FILENAME = ".project-docs-blueprints.json";
16
+ /** Diretórios que nunca entram na varredura do projeto. */
17
+ const SKIP_DIRS = new Set([
18
+ "node_modules",
19
+ ".git",
20
+ "dist",
21
+ ".obsidian",
22
+ HANDOFF_DIR,
23
+ ]);
24
+ async function exists(path) {
25
+ try {
26
+ await access(path, constants.F_OK);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ // --- Normalização (antes de comparar) -------------------------------------
34
+ //
35
+ // O achado central da recon: sem normalizar, o formatter do projeto faz todo
36
+ // arquivo parecer divergente. Normalizamos AMBOS os lados só pra COMPARAR — o
37
+ // que vai pro disco é sempre o conteúdo bruto do template.
38
+ /** Caminho do Prettier LOCAL do projeto, ou null se não estiver instalado. */
39
+ async function prettierBin(projectDir) {
40
+ const bin = join(projectDir, "node_modules", ".bin", process.platform === "win32" ? "prettier.cmd" : "prettier");
41
+ return (await exists(bin)) ? bin : null;
42
+ }
43
+ /** Roda o Prettier LOCAL do projeto (nunca baixa nada). null se indisponível. */
44
+ function runPrettier(projectDir, filename, content) {
45
+ return new Promise(async (resolve) => {
46
+ const bin = await prettierBin(projectDir);
47
+ if (bin === null) {
48
+ resolve(null);
49
+ return;
50
+ }
51
+ const child = spawn(bin, ["--stdin-filepath", filename], {
52
+ cwd: projectDir,
53
+ });
54
+ let out = "";
55
+ child.stdout.on("data", (d) => (out += d));
56
+ child.stderr.on("data", () => { });
57
+ child.on("error", () => resolve(null));
58
+ child.on("close", (code) => resolve(code === 0 ? out : null));
59
+ child.stdin.end(content);
60
+ });
61
+ }
62
+ /** Reduz links a só o texto visível, pra diferença de estilo não contar. */
63
+ function stripLinks(md) {
64
+ return md
65
+ .replace(/\[\[([^\]]+)\]\]/g, "$1") // [[wiki]]
66
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"); // [texto](url)
67
+ }
68
+ async function normalizeForCompare(content, opts) {
69
+ let s = content.replace(/\r\n/g, "\n");
70
+ if (opts.formatNormalize) {
71
+ const pretty = await runPrettier(opts.projectDir, opts.filename, s);
72
+ if (pretty !== null)
73
+ s = pretty;
74
+ }
75
+ s = stripLinks(s);
76
+ s = s
77
+ .split("\n")
78
+ .map((l) => l.replace(/[ \t]+$/, ""))
79
+ .join("\n")
80
+ .replace(/\n{3,}/g, "\n\n")
81
+ .trim();
82
+ return s + "\n";
83
+ }
84
+ /** Converte links markdown relativos pro estilo wiki `[[alvo]]` do Obsidian. */
85
+ function toWikiLinks(md) {
86
+ return md.replace(/\[([^\]]+)\]\(\.?\/?([^)]+?)(?:\.md)?\)/g, (_m, _text, target) => {
87
+ const base = target.split("/").pop() ?? target;
88
+ return `[[${base}]]`;
89
+ });
90
+ }
91
+ // --- Detecção de movidos --------------------------------------------------
92
+ /** basename ignorando o sufixo `.template` (ex.: _overview.template.md → _overview.md). */
93
+ function normBasename(path) {
94
+ const base = path.split("/").pop() ?? path;
95
+ return base.replace(/\.template(\.[^.]+)$/, "$1");
96
+ }
97
+ // --- Classificação --------------------------------------------------------
98
+ async function classify(art, templatePath, projectPath, manifest, opts) {
99
+ // 1. não existe no projeto → adicionar
100
+ if (projectPath === null) {
101
+ return { bucket: "add", reason: "novo no template; não existe no projeto" };
102
+ }
103
+ const projectRaw = await readFile(join(opts.targetDir, projectPath), "utf8");
104
+ const filename = projectPath.split("/").pop() ?? "x.md";
105
+ // 2. com manifesto: comparar hash bruto contra a base registrada
106
+ const base = manifest?.files[templatePath]?.sha256;
107
+ if (base) {
108
+ const untouched = sha256(projectRaw) === base;
109
+ const templateChanged = sha256(art.content) !== base;
110
+ if (untouched) {
111
+ return templateChanged
112
+ ? { bucket: "automerge", reason: "você não editou; template evoluiu" }
113
+ : { bucket: "uptodate", reason: "em dia (nada mudou)" };
114
+ }
115
+ }
116
+ // 3. iguais após normalizar → no-op (pega diff só de formatação/links)
117
+ const [np, nn] = await Promise.all([
118
+ normalizeForCompare(projectRaw, {
119
+ projectDir: opts.targetDir,
120
+ filename,
121
+ formatNormalize: opts.formatNormalize,
122
+ }),
123
+ normalizeForCompare(art.content, {
124
+ projectDir: opts.targetDir,
125
+ filename,
126
+ formatNormalize: opts.formatNormalize,
127
+ }),
128
+ ]);
129
+ if (np === nn) {
130
+ return { bucket: "uptodate", reason: "idêntico após normalizar formatação" };
131
+ }
132
+ // 4. divergência real → decisão humana (gera handoff)
133
+ return {
134
+ bucket: "review",
135
+ reason: base
136
+ ? "você editou e o template também mudou"
137
+ : "sem manifesto; divergência precisa de classificação",
138
+ };
139
+ }
140
+ // --- Plano + aplicação ----------------------------------------------------
141
+ async function readManifest(targetDir) {
142
+ // nome atual primeiro; cai pro legado pra ainda achar o cursor de projetos
143
+ // pré-rename (a migration 0.5.0 renomeia o arquivo no disco no mesmo update).
144
+ for (const name of [MANIFEST_FILENAME, LEGACY_MANIFEST_FILENAME]) {
145
+ const p = join(targetDir, "docs", name);
146
+ if (!(await exists(p)))
147
+ continue;
148
+ try {
149
+ return JSON.parse(await readFile(p, "utf8"));
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ function handoffContent(item, projectRaw) {
158
+ return `# Handoff de update — ${item.templatePath}
159
+
160
+ > Gerado pelo \`product-runner update\`. O CLI **não** alterou o arquivo
161
+ > original; isto é material pra você (ou uma sessão Claude) classificar e decidir.
162
+
163
+ - **Arquivo no projeto:** \`${item.projectPath}\`${item.moved ? " (movido em relação ao template)" : ""}
164
+ - **Origem no template:** \`${item.art.fromTemplate}\`
165
+ - **Motivo:** ${item.reason}
166
+
167
+ ## Tarefa
168
+
169
+ Classifique cada diferença entre as duas versões abaixo como **melhoria do
170
+ template** (trazer) ou **customização do projeto** (preservar), e produza a
171
+ versão final mesclada. Em caso de conflito real no mesmo trecho, explique o
172
+ tradeoff em vez de escolher sozinho.
173
+
174
+ ## Versão ATUAL (no projeto)
175
+
176
+ \`\`\`\`\`markdown
177
+ ${projectRaw.trimEnd()}
178
+ \`\`\`\`\`
179
+
180
+ ## Versão NOVA (template)
181
+
182
+ \`\`\`\`\`markdown
183
+ ${item.art.content.trimEnd()}
184
+ \`\`\`\`\`
185
+ `;
186
+ }
187
+ function migrationHandoff(mig) {
188
+ return `# Migration ${mig.version} — ${mig.title}
189
+
190
+ > Migration **conduzida** (não-automática) do \`product-runner\`. O CLI
191
+ > não aplicou nada; conduza as instruções abaixo com o humano.
192
+ ${mig.previous ? `\n- **De:** ${mig.previous} → **${mig.version}**` : ""}
193
+ ${mig.risk ? `- **Risco:** ${mig.risk}` : ""}
194
+ ${mig.affects.length ? `- **Afeta:** ${mig.affects.join(", ")}` : ""}
195
+
196
+ ${mig.body}
197
+ `;
198
+ }
199
+ /**
200
+ * Calcula o plano de update e, se !dryRun, aplica: escreve ADD + AUTO-MERGE
201
+ * (ADD primeiro, pra refs resolverem), gera handoff dos REVIEW sem tocar o
202
+ * original, e reescreve o manifesto com a nova base.
203
+ */
204
+ export async function update(opts) {
205
+ const manifest = await readManifest(opts.targetDir);
206
+ const mode = manifest ? "3way" : "legacy";
207
+ const profile = opts.profile ?? manifest?.profile;
208
+ if (!profile) {
209
+ throw new Error("Sem manifesto e sem --profile. Informe --profile <cli|ssr> pra um projeto legado.");
210
+ }
211
+ // nome/porta: do manifesto, ou inferidos (só afetam o CLAUDE.md, que é review)
212
+ let name = manifest?.projectName;
213
+ let port = manifest?.port ?? "3000";
214
+ if (!name) {
215
+ const claudePath = join(opts.targetDir, "CLAUDE.md");
216
+ if (await exists(claudePath)) {
217
+ const m = (await readFile(claudePath, "utf8")).match(/^#\s+(.+)$/m);
218
+ name = m?.[1]?.trim();
219
+ }
220
+ name = name ?? "projeto";
221
+ }
222
+ const meta = { name, profile, port };
223
+ // Normalização por Prettier só roda se pedida E o binário existir no projeto.
224
+ // Se foi pedida mas não há binário, degradamos — e sinalizamos no resultado
225
+ // (senão arquivos que só diferem por formatação caem em REVISAR sem aviso).
226
+ const prettierFound = (await prettierBin(opts.targetDir)) !== null;
227
+ const formatActive = opts.formatNormalize && prettierFound;
228
+ const effectiveOpts = { ...opts, formatNormalize: formatActive };
229
+ const handoffDir = join(opts.targetDir, "docs", HANDOFF_DIR);
230
+ // Migrations: só com manifesto (cursor conhecido). Intervalo
231
+ // (manifesto.version, versão-do-pacote]. Rodam ANTES do diff de estado.
232
+ const pkgVersion = await packageVersion(templatesRoot());
233
+ const migrations = manifest
234
+ ? migrationsInSpan(await discoverMigrations(join(templatesRoot(), "migrations")), manifest.version, pkgVersion)
235
+ : [];
236
+ if (!opts.dryRun && migrations.length) {
237
+ // 1. ops mecânicos das migrations autoApply, em ordem (mutam o projeto)
238
+ for (const mig of migrations) {
239
+ if (mig.autoApply && mig.ops.length) {
240
+ await applyOps(opts.targetDir, mig.ops, await listProjectFiles(opts.targetDir));
241
+ }
242
+ }
243
+ // 2. corpo conduzido → handoff (mesmo em autoApply: parte mecânica roda
244
+ // sozinha, mas o que precisa de decisão humana fica em MIGRATION-<v>.md)
245
+ const withBody = migrations.filter((m) => m.body.trim().length > 0);
246
+ if (withBody.length) {
247
+ await mkdir(handoffDir, { recursive: true });
248
+ for (const mig of withBody) {
249
+ await writeFile(join(handoffDir, `MIGRATION-${mig.version}.md`), migrationHandoff(mig), "utf8");
250
+ }
251
+ }
252
+ }
253
+ const artifacts = await buildArtifacts(meta);
254
+ // índice de arquivos do projeto por basename normalizado (pra detectar movidos)
255
+ // (já reflete o resultado das migrations autoApply, aplicadas acima)
256
+ const projectFiles = await listProjectFiles(opts.targetDir);
257
+ const byBasename = new Map();
258
+ for (const p of projectFiles) {
259
+ const key = normBasename(p);
260
+ const list = byBasename.get(key) ?? [];
261
+ list.push(p);
262
+ byBasename.set(key, list);
263
+ }
264
+ const onlyRe = opts.only ? globToRegExp(opts.only) : null;
265
+ const templatePaths = new Set(artifacts.keys());
266
+ const plan = [];
267
+ for (const [templatePath, art] of artifacts) {
268
+ // resolve onde (e se) o arquivo existe no projeto
269
+ let projectPath = null;
270
+ let moved = false;
271
+ if (await exists(join(opts.targetDir, templatePath))) {
272
+ projectPath = templatePath;
273
+ }
274
+ else {
275
+ // candidatos a "movido": mesmo basename, mas NÃO um arquivo que já é
276
+ // emitido pelo template no próprio lugar dele (evita casar docs/README.md
277
+ // com docs/agents/README.md, p.ex.).
278
+ const candidates = (byBasename.get(normBasename(templatePath)) ?? []).filter((p) => !templatePaths.has(p));
279
+ if (candidates.length === 1) {
280
+ projectPath = candidates[0];
281
+ moved = projectPath !== templatePath;
282
+ }
283
+ }
284
+ if (onlyRe && !onlyRe.test(templatePath) && !(projectPath && onlyRe.test(projectPath))) {
285
+ continue;
286
+ }
287
+ const { bucket, reason } = await classify(art, templatePath, projectPath, manifest, effectiveOpts);
288
+ plan.push({
289
+ templatePath,
290
+ projectPath: projectPath ?? templatePath,
291
+ bucket,
292
+ reason,
293
+ moved,
294
+ art,
295
+ });
296
+ }
297
+ const counts = {
298
+ add: 0,
299
+ automerge: 0,
300
+ uptodate: 0,
301
+ review: 0,
302
+ };
303
+ for (const it of plan)
304
+ counts[it.bucket]++;
305
+ if (!opts.dryRun) {
306
+ const projectUsesWiki = await detectWikiLinks(opts.targetDir, projectFiles);
307
+ // ADD primeiro (pra refs de arquivos novos resolverem nos merges)
308
+ for (const it of plan.filter((i) => i.bucket === "add")) {
309
+ let content = it.art.content;
310
+ if (opts.normalizeLinks && projectUsesWiki)
311
+ content = toWikiLinks(content);
312
+ const dest = join(opts.targetDir, ...it.templatePath.split("/"));
313
+ await mkdir(dirname(dest), { recursive: true });
314
+ await writeFile(dest, content, "utf8");
315
+ }
316
+ // AUTO-MERGE: substitui no lugar atual (inclusive se movido)
317
+ for (const it of plan.filter((i) => i.bucket === "automerge")) {
318
+ let content = it.art.content;
319
+ if (opts.normalizeLinks && projectUsesWiki)
320
+ content = toWikiLinks(content);
321
+ const dest = join(opts.targetDir, ...it.projectPath.split("/"));
322
+ await mkdir(dirname(dest), { recursive: true });
323
+ await writeFile(dest, content, "utf8");
324
+ }
325
+ // REVIEW: gera handoff, nunca toca o original
326
+ const reviews = plan.filter((i) => i.bucket === "review");
327
+ if (reviews.length) {
328
+ await mkdir(handoffDir, { recursive: true });
329
+ // limpa só handoffs antigos; preserva o resto (ex.: marcador .last-check)
330
+ for (const f of await readdir(handoffDir)) {
331
+ if (f.endsWith(".handoff.md")) {
332
+ await rm(join(handoffDir, f), { force: true });
333
+ }
334
+ }
335
+ for (const it of reviews) {
336
+ const projectRaw = await readFile(join(opts.targetDir, it.projectPath), "utf8");
337
+ const flat = it.templatePath.replace(/\//g, "__");
338
+ await writeFile(join(handoffDir, `${flat}.handoff.md`), handoffContent(it, projectRaw), "utf8");
339
+ }
340
+ }
341
+ // reescreve o manifesto: base passa a ser o template atual (vira 3-way no próximo)
342
+ await writeManifest(join(opts.targetDir, "docs"), meta, artifacts);
343
+ }
344
+ return {
345
+ mode,
346
+ plan,
347
+ applied: !opts.dryRun,
348
+ handoffDir,
349
+ counts,
350
+ formatActive,
351
+ prettierFound,
352
+ migrations,
353
+ };
354
+ }
355
+ /** Lista arquivos do projeto (POSIX, relativos a targetDir), pulando SKIP_DIRS. */
356
+ async function listProjectFiles(targetDir) {
357
+ const all = await listFiles(targetDir);
358
+ return all.filter((p) => !p.split("/").some((seg) => SKIP_DIRS.has(seg)));
359
+ }
360
+ /** Heurística: o projeto usa predominantemente wiki-links `[[x]]`? */
361
+ async function detectWikiLinks(targetDir, projectFiles) {
362
+ let wiki = 0;
363
+ for (const p of projectFiles.filter((f) => f.endsWith(".md")).slice(0, 50)) {
364
+ const content = await readFile(join(targetDir, p), "utf8");
365
+ wiki += (content.match(/\[\[[^\]]+\]\]/g) ?? []).length;
366
+ }
367
+ return wiki >= 3;
368
+ }
369
+ const BUCKET_LABEL = {
370
+ add: "➕ ADICIONA",
371
+ automerge: "✅ AUTO-MERGE",
372
+ uptodate: "✓ EM DIA",
373
+ review: "⚠️ REVISAR",
374
+ };
375
+ /** Renderiza o plano pra impressão no terminal. */
376
+ export function renderPlan(result) {
377
+ const lines = [];
378
+ if (result.migrations.length) {
379
+ lines.push(`🧭 MIGRATIONS no caminho (${result.migrations.length}) — em ordem, antes do diff:`);
380
+ for (const mig of result.migrations) {
381
+ const mode = mig.autoApply ? "automático" : "conduzir";
382
+ const risk = mig.risk ? ` · risco ${mig.risk}` : "";
383
+ lines.push(` ${mig.version} ${mig.title} [${mode}${risk}]`);
384
+ }
385
+ lines.push("");
386
+ }
387
+ const order = ["add", "automerge", "review", "uptodate"];
388
+ for (const bucket of order) {
389
+ const items = result.plan.filter((i) => i.bucket === bucket);
390
+ if (!items.length)
391
+ continue;
392
+ lines.push(`${BUCKET_LABEL[bucket]} (${items.length})`);
393
+ for (const it of items) {
394
+ const path = it.moved ? `${it.projectPath} ← ${it.templatePath}` : it.templatePath;
395
+ lines.push(` ${path} — ${it.reason}`);
396
+ }
397
+ lines.push("");
398
+ }
399
+ return lines.join("\n").trimEnd();
400
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ {
3
+ "version": "0.3.0",
4
+ "previous": "0.2.3",
5
+ "title": "agente-prod-runner como entrada; _overview/_open-issues vão pra specs/ sem .template",
6
+ "risk": "low",
7
+ "autoApply": true,
8
+ "affects": [
9
+ "docs/_overview.template.md -> specs/_overview.md",
10
+ "docs/_open-issues.template.md -> specs/_open-issues.md",
11
+ "CLAUDE.md (seção Manutenção -> agente-prod-runner)"
12
+ ],
13
+ "ops": [
14
+ { "type": "rename", "from": "docs/_overview.template.md", "to": "specs/_overview.md" },
15
+ { "type": "rename", "from": "docs/_open-issues.template.md", "to": "specs/_open-issues.md" },
16
+ { "type": "replace", "glob": "**/*.md", "find": "_overview\\.template", "replace": "_overview" },
17
+ { "type": "replace", "glob": "**/*.md", "find": "_open-issues\\.template", "replace": "_open-issues" }
18
+ ]
19
+ }
20
+ ---
21
+
22
+ ## O que mudou na 0.3.0
23
+
24
+ Dois movimentos:
25
+
26
+ 1. **`_overview` / `_open-issues` saem de `docs/` (com sufixo `.template`) e viram
27
+ seeds em `specs/`** — `specs/_overview.md` e `specs/_open-issues.md`. É onde os
28
+ projetos reais já os colocavam; o `pipeline.md` sempre tratou specs como
29
+ habitantes de `specs/`. Os `ops` mecânicos acima fazem o rename e dropam o
30
+ sufixo `.template` das referências.
31
+
32
+ 2. **A porta de entrada agora é o `agente-prod-runner`** (roteador + ciclo de vida), e a
33
+ rotina de manutenção saiu do `CLAUDE.md` pro `docs/agents/agente-prod-runner.md`. O
34
+ `update` adiciona `agente-prod-runner.md` (e `agente-kickoff.md`, se faltar) em
35
+ `docs/agents/` automaticamente.
36
+
37
+ ## Conduza com o humano (parte não-mecânica)
38
+
39
+ O `CLAUDE.md` é customizado, então o `update` o marca como REVISAR — não é tocado
40
+ automaticamente. Ao reconciliar o handoff do `CLAUDE.md`:
41
+
42
+ - **Substitua a seção `## Manutenção dos protocolos de doc` antiga** (a que trazia
43
+ a rotina completa de verificação/`update`) **pelo gatilho fino** da versão nova,
44
+ que apenas aponta para `docs/agents/agente-prod-runner.md`. **Preserve** todo o resto do
45
+ seu `CLAUDE.md` (stack, arquitetura, design system, tabela de estado).
46
+ - Se o seu `CLAUDE.md` ainda **não** tinha a seção Manutenção (projeto bem antigo),
47
+ apenas **adicione** o gatilho fino.
48
+
49
+ Depois, confira:
50
+
51
+ - `specs/_overview.md` e `specs/_open-issues.md` existem e mantêm o conteúdo que
52
+ você havia preenchido (o rename preserva o conteúdo; só muda o caminho/nome).
53
+ - Nenhum link aponta mais para `*_overview.template` / `*_open-issues.template`.
54
+ Se sobrou algum link com caminho `docs/_overview…`, ajuste para `specs/_overview.md`.
@@ -0,0 +1,76 @@
1
+ ---
2
+ {
3
+ "version": "0.4.0",
4
+ "previous": "0.3.0",
5
+ "title": "status content-derived + fluxos de review como estágio nomeado",
6
+ "risk": "low",
7
+ "autoApply": false,
8
+ "affects": [
9
+ "docs/pipeline.md (estágio 5 + seção 'Em que estágio estou?')",
10
+ "docs/agents/README.md (fluxos de review na tabela/diagrama)",
11
+ "docs/lessons-learned.md (nova lição)",
12
+ "CLAUDE.md (subseção 'Em que etapa o projeto está' + caveat na tabela de estado)",
13
+ "specs/_overview.md (caveat 'índice derivado')"
14
+ ],
15
+ "ops": []
16
+ }
17
+ ---
18
+
19
+ ## O que mudou na 0.4.0
20
+
21
+ Um eixo só: **status do projeto se descobre pelo conteúdo das specs, não pela
22
+ tabela de índice** — e os **fluxos de review** (Estágio 5) passam a ser estágio
23
+ nomeado e verificável, não um passo solto.
24
+
25
+ Motivação concreta: numa sessão de orientação, ao responder "em que etapa
26
+ estamos / qual o próximo passo", o agente respondeu a partir das tabelas de
27
+ status (`_overview`, tabela de estado do `CLAUDE.md`) — lidas truncadas — sem
28
+ abrir nenhuma spec. Resultado: quase repassou a divergência do índice (que
29
+ driftou) como verdade e confundiu "Decisões de implementação preenchidas"
30
+ (rastro do **estágio 4**, por M1) com "review feito" (**estágio 5**, que tem
31
+ rastro próprio). A correção torna o procedimento explícito.
32
+
33
+ Mudanças por arquivo:
34
+
35
+ 1. **`docs/pipeline.md`** — o diagrama expande o Estágio 5 na sub-cadeia
36
+ **Review.Code → User Review → Review.Product → Review.LLM** (com escala de
37
+ Impeditivo); §5 deixa de ser "fase futura"; nova seção **"Em que estágio
38
+ estou?"** com a tabela de rastros e a regra de fonte (a spec é fonte;
39
+ `_overview`/tabela de estado são índice derivado).
40
+ 2. **`docs/agents/README.md`** — os quatro `agente-review-*` entram na tabela,
41
+ no diagrama e na prosa (antes não apareciam como estágio).
42
+ 3. **`docs/lessons-learned.md`** — nova lição "Status se lê do conteúdo, não do
43
+ índice".
44
+
45
+ Os três acima são arquivos de template **não-customizados** — o diff de estado
46
+ do `update` os reconcilia sozinho. Nada a conduzir neles.
47
+
48
+ ## Conduza com o humano (parte não-mecânica)
49
+
50
+ Os dois arquivos abaixo são **customizados** no seu projeto, então o `update` os
51
+ marca como REVISAR e **não** os toca automaticamente. A mudança aqui é
52
+ **aditiva** — preserve todo o seu conteúdo e apenas inclua o que falta:
53
+
54
+ - **`CLAUDE.md`:**
55
+ - No item do `pipeline` em "Docs de referência", **complete a cadeia** com o
56
+ estágio de review: `… → implementação → review (Review.Code → User Review →
57
+ Review.Product → Review.LLM)`.
58
+ - Na seção **"Estado do refactor / desenvolvimento"**, adicione o caveat
59
+ **"Índice derivado, não fonte"** acima da tabela (a tabela e o `_overview`
60
+ são conveniência que drift-a; a fonte é o conteúdo da spec).
61
+ - Adicione a subseção **"Em que etapa o projeto está"** (descobrir o estágio
62
+ pelo rastro no conteúdo da spec; cuidado para não confundir "Decisões de
63
+ implementação" = estágio 4 com review = estágio 5; nunca responder status de
64
+ leitura truncada `head -N`). Veja o texto de referência em
65
+ `common/claude-md.template.md` do pacote.
66
+
67
+ - **`specs/_overview.md`:** adicione no header o caveat **"Índice derivado, não
68
+ fonte"** — não responder "qual a próxima etapa" a partir desta tabela,
69
+ confirmar na spec. Mesmo princípio LDoc→HDoc.
70
+
71
+ Depois, confira:
72
+
73
+ - `docs/pipeline.md` tem a seção "Em que estágio estou?" e o Estágio 5 com a
74
+ sub-cadeia de review.
75
+ - O `CLAUDE.md` aponta para essa seção e a tabela de estado está marcada como
76
+ índice derivado.
@@ -0,0 +1,55 @@
1
+ ---
2
+ {
3
+ "version": "0.5.0",
4
+ "previous": "0.4.0",
5
+ "title": "rename: project-docs-blueprints -> product-runner (e pdb -> prod-runner)",
6
+ "risk": "low",
7
+ "autoApply": true,
8
+ "affects": [
9
+ "docs/.project-docs-blueprints.json -> docs/.product-runner.json",
10
+ "docs/agents/agente-pdb.md -> docs/agents/agente-prod-runner.md",
11
+ "docs/.pdb-update/ -> docs/.prod-runner-update/ (efêmero, regenera)",
12
+ "CLAUDE.md / docs customizados (referências a `npx project-docs-blueprints`, `agente-pdb`, `.pdb-update`)"
13
+ ],
14
+ "ops": [
15
+ { "type": "rename", "from": "docs/.project-docs-blueprints.json", "to": "docs/.product-runner.json" },
16
+ { "type": "rename", "from": "docs/agents/agente-pdb.md", "to": "docs/agents/agente-prod-runner.md" }
17
+ ]
18
+ }
19
+ ---
20
+
21
+ ## O que mudou na 0.5.0
22
+
23
+ O pacote foi renomeado de **`project-docs-blueprints`** para **`product-runner`**,
24
+ e a abreviação **`pdb`** virou **`prod-runner`**:
25
+
26
+ - `agente-pdb.md` → `agente-prod-runner.md` (a porta de entrada / roteador)
27
+ - `.pdb-update/` → `.prod-runner-update/` (pasta efêmera de handoffs)
28
+ - diretivas `pdb-merge` → `prod-runner-merge` (internas ao pacote; não vão pra projetos)
29
+
30
+ Mudança de identidade/marca — nada de comportamento.
31
+
32
+ As partes mecânicas são aplicadas sozinhas pelos `ops`: o manifesto
33
+ `docs/.project-docs-blueprints.json` → `docs/.product-runner.json` e o agente
34
+ `docs/agents/agente-pdb.md` → `docs/agents/agente-prod-runner.md`. O `update`
35
+ ainda tem um **fallback** que lê o manifesto no nome antigo se o novo não
36
+ existir, então rodar o `update` de um projeto pré-rename funciona.
37
+
38
+ A pasta `docs/.pdb-update/` é efêmera e gitignored — **não** é renomeada por op;
39
+ ela simplesmente regenera como `docs/.prod-runner-update/` no próximo handoff.
40
+ Pode apagar a antiga.
41
+
42
+ ## Conduza com o humano (parte não-mecânica)
43
+
44
+ Em arquivos **customizados** (que o `update` não toca), troque referências ao
45
+ nome antigo:
46
+
47
+ - `CLAUDE.md` — `npx project-docs-blueprints …` → `npx product-runner …`; o
48
+ ponteiro de manutenção `agente-pdb` → `agente-prod-runner`; o aviso de
49
+ `.gitignore` de `docs/.pdb-update/` → `docs/.prod-runner-update/`.
50
+ - `.gitignore` do projeto — troque a linha `docs/.pdb-update/` por
51
+ `docs/.prod-runner-update/`.
52
+ - Qualquer script/CI/README seu que invoque o pacote ou cite `agente-pdb`.
53
+
54
+ Busca rápida: `grep -rni "project-docs-blueprints\|pdb" .` — deve sobrar só
55
+ histórico (CHANGELOG/migrations).
@@ -0,0 +1,68 @@
1
+ # Migrations
2
+
3
+ Notas de migração entre versões dos templates. **Opcionais por versão** — a
4
+ maioria dos bumps (um arquivo novo, formatação, pequena adição) é resolvida pelo
5
+ diff de estado do `update` e **não** precisa de migration. Uma migration só
6
+ existe quando há mudança que o diff não expressa ou não deve aplicar sozinho:
7
+ rename/split de arquivos, mudança de convenção, ou transformação acoplada a
8
+ código.
9
+
10
+ ## Como o `update` usa
11
+
12
+ 1. Lê o `version` do manifesto do projeto (o "cursor").
13
+ 2. Junta as migrations no intervalo `(cursor, versão-do-pacote]`, em ordem.
14
+ 3. Roda-as **antes** do diff de estado: as `autoApply` aplicam seus `ops`
15
+ mecânicos; as demais viram handoff (`docs/.prod-runner-update/MIGRATION-<v>.md`) pra
16
+ conduzir com o humano.
17
+ 4. Depois o diff de estado reconcilia o que sobrou.
18
+
19
+ ## Formato
20
+
21
+ Um arquivo por versão: `migrations/<x.y.z>.md`. Frontmatter **JSON** entre `---`
22
+ (o pacote é zero-dependência; JSON.parse é robusto), seguido do corpo em
23
+ markdown com as instruções conduzidas.
24
+
25
+ ```md
26
+ ---
27
+ {
28
+ "version": "0.3.0",
29
+ "previous": "0.2.3",
30
+ "title": "Resumo curto da migração",
31
+ "risk": "high",
32
+ "autoApply": false,
33
+ "affects": ["docs/DESIGN-SYSTEM.md"],
34
+ "ops": []
35
+ }
36
+ ---
37
+
38
+ ## O que mudou
39
+
40
+ Prosa explicando a mudança e, se `autoApply: false`, como conduzir a decisão
41
+ com o humano (o que trazer, o que preservar, qual o tradeoff).
42
+ ```
43
+
44
+ ### Campos
45
+
46
+ | Campo | Significado |
47
+ |---|---|
48
+ | `version` | versão que esta migration leva o projeto a alcançar (= nome do arquivo) |
49
+ | `previous` | versão anterior (informativo) |
50
+ | `title` | resumo de uma linha |
51
+ | `risk` | `low` ou `high` |
52
+ | `autoApply` | `true` = o CLI aplica os `ops` sozinho; `false` = só apresenta, a LLM conduz |
53
+ | `affects` | lista informativa de arquivos/áreas afetados |
54
+ | `ops` | passos mecânicos (só executados se `autoApply: true`) |
55
+
56
+ ### Ops mecânicos
57
+
58
+ ```json
59
+ { "type": "rename", "from": "docs/a.md", "to": "docs/b.md" }
60
+ { "type": "replace", "glob": "docs/**/*.md", "find": "regex", "replace": "texto" }
61
+ ```
62
+
63
+ - `rename`: move o arquivo (no-op se a origem não existir — ex.: já renomeado).
64
+ - `replace`: regex global nos arquivos que casam o `glob` (`*` num segmento,
65
+ `**` atravessa).
66
+
67
+ > Mudança acoplada a código (ex.: migração de tokens primitivos → semânticos)
68
+ > **não** deve ser `autoApply` — descreva em prosa e deixe virar spec/issue.