@spec-wave/cli 0.11.1 → 0.11.2

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/README.md CHANGED
@@ -91,7 +91,20 @@ Ferramenta Node.js que configura e opera o fluxo via linha de comando.
91
91
  gh issue edit 12 --add-label "spec-wave:force" --add-label "spec-wave:decompose"
92
92
  ```
93
93
 
94
- No `decompose` ela **fecha as sub-issues da decomposição anterior** (Stories e suas Tasks) antes de gerar as novas — operação destrutiva, com aviso no comentário da issue quando alguma já saiu de `✅ Ready`. Em `generate-spec`/`generate-plan` não muda nada: essas etapas sobrescrevem o documento a cada acionamento. A flag equivalente para execução local é `--force`.
94
+ No `decompose` ela **fecha as sub-issues da decomposição anterior** (Stories e suas Tasks) antes de gerar as novas — operação destrutiva, com aviso no comentário da issue quando alguma já saiu de `✅ Ready`. Em `generate-spec`/`generate-plan` ela **versiona** o documento em vez de sobrescrever (veja abaixo). A flag equivalente para execução local é `--force`.
95
+
96
+ ### Documentos versionados
97
+
98
+ A primeira geração escreve `spec.md`/`plan.md` (v1). Cada regeração **forçada** grava a próxima versão ao lado, preservando as anteriores:
99
+
100
+ ```
101
+ docs/features/<slug>/
102
+ spec.md ← v1
103
+ spec-v2.md ← regeração forçada ⬅ spec atual
104
+ plan.md ⬅ plano atual (spec e plan versionam independente)
105
+ ```
106
+
107
+ A **maior versão é o documento atual**: é ela que o `generate-plan` usa como contexto, que o `validate` valida, e que o `decompose` e o `implement` leem. Uma regeração **sem** force sobrescreve essa versão atual.
95
108
 
96
109
  ### Skill (`src/templates/skill/SKILL.md`)
97
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { resolveToken } from '../api/auth.mjs';
3
3
  import { getIssue, createIssue, removeLabel, addLabel, commentOnIssue, addBlockedBy, closeIssue } from '../api/github-rest.mjs';
4
4
  import { addSubIssue, listSubIssues, addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
5
5
  import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
6
6
  import { isForced, consumeForceLabel } from '../lib/force.mjs';
7
+ import { resolveDoc } from '../lib/feature-docs.mjs';
7
8
  import { generateDocument } from '../lib/claude.mjs';
8
9
  import { runCritique } from '../lib/critique.mjs';
9
10
  import { recordUsage } from '../lib/usage-report.mjs';
@@ -285,14 +286,18 @@ async function decomposeFeature(ctx) {
285
286
  const slug = slugify(issue.title);
286
287
  const featureDir = `docs/features/${slug}`;
287
288
 
288
- const planContent = existsSync(`${featureDir}/plan.md`)
289
- ? readFileSync(`${featureDir}/plan.md`, 'utf-8')
290
- : '(plan.md não encontrado)';
291
- const specContent = existsSync(`${featureDir}/spec.md`)
292
- ? readFileSync(`${featureDir}/spec.md`, 'utf-8')
293
- : '(spec.md não encontrado)';
289
+ // Documentos ATUAIS (maior versão): com regerações forçadas o vigente é
290
+ // spec-v2.md/plan-v3.md…, não o arquivo original.
291
+ const spec = resolveDoc(featureDir, 'spec');
292
+ const plan = resolveDoc(featureDir, 'plan');
293
+ const specContent = spec ? readFileSync(spec.path, 'utf-8') : '(spec.md não encontrado)';
294
+ const planContent = plan ? readFileSync(plan.path, 'utf-8') : '(plan.md não encontrado)';
294
295
 
295
296
  console.log(`Decompondo Feature: ${issue.title}`);
297
+ if (spec || plan) {
298
+ console.log(`Documentos: ${spec ? `${spec.path} (v${spec.version})` : 'spec ausente'} · ` +
299
+ `${plan ? `${plan.path} (v${plan.version})` : 'plano ausente'}`);
300
+ }
296
301
  const userContent = [
297
302
  `Feature: ${issue.title}`,
298
303
  `Issue #${issueNumber}`,
@@ -308,8 +313,8 @@ async function decomposeFeature(ctx) {
308
313
  try {
309
314
  critique = await runCritique({
310
315
  kind: 'stories',
311
- spec: existsSync(`${featureDir}/spec.md`) ? specContent : null,
312
- plan: existsSync(`${featureDir}/plan.md`) ? planContent : null,
316
+ spec: spec ? specContent : null,
317
+ plan: plan ? planContent : null,
313
318
  stories: decomposition.stories,
314
319
  usage,
315
320
  });
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
2
+ import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
3
3
  import { resolveToken } from '../api/auth.mjs';
4
4
  import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
5
5
  import { detectIssueType } from '../lib/issue-type.mjs';
@@ -9,6 +9,7 @@ import { runCritique } from '../lib/critique.mjs';
9
9
  import { recordUsage } from '../lib/usage-report.mjs';
10
10
  import { slugify } from '../lib/slugify.mjs';
11
11
  import { isForced, consumeForceLabel } from '../lib/force.mjs';
12
+ import { resolveDoc, resolveWritePath } from '../lib/feature-docs.mjs';
12
13
  import { buildTechContext } from '../lib/tech-context.mjs';
13
14
 
14
15
  // Aviso anexado ao comentário quando o lint de idioma ainda reprova após o
@@ -70,18 +71,22 @@ export async function generatePlan({ issueNumber, force = false }) {
70
71
  return;
71
72
  }
72
73
 
73
- // Ver generate-spec: não guard aqui a label regera e sobrescreve.
74
+ // Force: grava a PRÓXIMA versão (plan-v2.md…) preservando as anteriores; sem
75
+ // force, sobrescreve a versão vigente. Ver lib/feature-docs.mjs.
74
76
  const forced = isForced({ labels: issue.labels || [], flag: force });
75
77
  await consumeForceLabel(token, owner, repo, parseInt(issueNumber, 10));
76
- if (forced) console.log('Modo forçado ativo — plan.md será regerado (sobrescreve o existente).');
77
78
 
78
79
  const slug = slugify(issue.title);
79
80
  const featureDir = `docs/features/${slug}`;
80
- const filePath = `${featureDir}/plan.md`;
81
+ const target = resolveWritePath(featureDir, 'plan', { force: forced });
82
+ const filePath = target.path;
83
+ if (forced) console.log(`Modo forçado ativo — gerando ${filePath} (v${target.version}), sem tocar nas versões anteriores.`);
81
84
 
82
- // Read existing spec.md if available (spec é gerada antes do plano)
83
- const specPath = `${featureDir}/spec.md`;
84
- const specContent = existsSync(specPath) ? readFileSync(specPath, 'utf-8') : null;
85
+ // Spec ATUAL (maior versão) como contexto — a spec é gerada antes do plano.
86
+ const spec = resolveDoc(featureDir, 'spec');
87
+ const specPath = spec?.path || `${featureDir}/spec.md`;
88
+ const specContent = spec ? readFileSync(spec.path, 'utf-8') : null;
89
+ if (spec) console.log(`Usando ${spec.path} (spec v${spec.version}) como contexto.`);
85
90
 
86
91
  // Tech context (RFC-002 §4): estático + dinâmico + override do corpo da issue.
87
92
  const tech = buildTechContext({ issueBody: issue.body || '' });
@@ -119,7 +124,7 @@ export async function generatePlan({ issueNumber, force = false }) {
119
124
  git(`git config user.email "spec-wave[bot]@github.com"`);
120
125
  git(`git config user.name "spec-wave[bot]"`);
121
126
  git(`git add "${filePath}"`);
122
- git(`git commit -m "docs: generate plan.md for ${slug} [spec-wave]"`);
127
+ git(`git commit -m "docs: generate plan v${target.version} for ${slug} [spec-wave]"`);
123
128
  git('git pull --rebase');
124
129
  git('git push');
125
130
 
@@ -129,8 +134,12 @@ export async function generatePlan({ issueNumber, force = false }) {
129
134
  // Comment on issue
130
135
  await commentOnIssue(
131
136
  token, owner, repo, parseInt(issueNumber, 10),
132
- `📋 **plan.md gerado automaticamente!**\n\n` +
137
+ `📋 **plano técnico gerado automaticamente** (v${target.version})\n\n` +
133
138
  `📄 Arquivo: [\`${filePath}\`](https://github.com/${owner}/${repo}/blob/main/${filePath})\n\n` +
139
+ (target.isNewVersion
140
+ ? `♻️ Regeração forçada: as versões anteriores foram **preservadas**; ` +
141
+ `**v${target.version} passa a ser o plano atual** — é ele que a validação e a decomposição vão ler.\n\n`
142
+ : '') +
134
143
  `Revise o plano e, quando estiver pronto, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
135
144
  `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\`` +
136
145
  formatLintWarning(lintFindings)
@@ -6,6 +6,7 @@ import { generateDocument } from '../lib/claude.mjs';
6
6
  import { recordUsage } from '../lib/usage-report.mjs';
7
7
  import { slugify } from '../lib/slugify.mjs';
8
8
  import { isForced, consumeForceLabel } from '../lib/force.mjs';
9
+ import { resolveWritePath } from '../lib/feature-docs.mjs';
9
10
  import { detectIssueType } from '../lib/issue-type.mjs';
10
11
  import { allowsSpecPlan, SPEC_PLAN_EXCLUDED_TYPES, TARGET_LANGUAGE } from '../config.mjs';
11
12
 
@@ -71,16 +72,17 @@ export async function generateSpec({ issueNumber, force = false }) {
71
72
  return;
72
73
  }
73
74
 
74
- // A geração de spec/plan não tem guard — re-acionar a label regera e
75
- // sobrescreve o arquivo. O force existe por simetria com o decompose (e para
76
- // a label ser consumida quando aplicada aos três de uma vez).
75
+ // Force (flag --force ou label spec-wave:force): em vez de sobrescrever,
76
+ // grava a PRÓXIMA versão (spec-v2.md, spec-v3.md…) e ela passa a ser o
77
+ // documento atual. Sem force, sobrescreve a versão vigente.
77
78
  const forced = isForced({ labels: issue.labels || [], flag: force });
78
79
  await consumeForceLabel(token, owner, repo, parseInt(issueNumber, 10));
79
- if (forced) console.log('Modo forçado ativo — spec.md será regerado (sobrescreve o existente).');
80
80
 
81
81
  const slug = slugify(issue.title);
82
82
  const featureDir = `docs/features/${slug}`;
83
- const filePath = `${featureDir}/spec.md`;
83
+ const target = resolveWritePath(featureDir, 'spec', { force: forced });
84
+ const filePath = target.path;
85
+ if (forced) console.log(`Modo forçado ativo — gerando ${filePath} (v${target.version}), sem tocar nas versões anteriores.`);
84
86
 
85
87
  // Payload estruturado (RFC-002 §5.1): metadata + entrada de negócio.
86
88
  const payload = {
@@ -115,7 +117,7 @@ export async function generateSpec({ issueNumber, force = false }) {
115
117
  git(`git config user.email "spec-wave[bot]@github.com"`);
116
118
  git(`git config user.name "spec-wave[bot]"`);
117
119
  git(`git add "${filePath}"`);
118
- git(`git commit -m "docs: generate spec.md for ${slug} [spec-wave]"`);
120
+ git(`git commit -m "docs: generate spec v${target.version} for ${slug} [spec-wave]"`);
119
121
  git('git pull --rebase');
120
122
  git('git push');
121
123
 
@@ -125,8 +127,12 @@ export async function generateSpec({ issueNumber, force = false }) {
125
127
  // Comment on issue
126
128
  await commentOnIssue(
127
129
  token, owner, repo, parseInt(issueNumber, 10),
128
- `📋 **spec.md gerado automaticamente!**\n\n` +
130
+ `📋 **spec gerada automaticamente** (v${target.version})\n\n` +
129
131
  `📄 Arquivo: [\`${filePath}\`](https://github.com/${owner}/${repo}/blob/main/${filePath})\n\n` +
132
+ (target.isNewVersion
133
+ ? `♻️ Regeração forçada: as versões anteriores foram **preservadas**; ` +
134
+ `**v${target.version} passa a ser a spec atual** — é ela que o plano, a validação e a decomposição vão ler.\n\n`
135
+ : '') +
130
136
  `Revise a especificação e, quando estiver pronto, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
131
137
  `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\`` +
132
138
  formatLintWarning(lintFindings)
@@ -14,6 +14,7 @@ import { detectIssueType } from '../lib/issue-type.mjs';
14
14
  import { slugify } from '../lib/slugify.mjs';
15
15
  import { parseDependencies, orderStories, formatDependencyLine } from '../lib/dependencies.mjs';
16
16
  import { loadProjectConfig, resolveField } from '../lib/board.mjs';
17
+ import { resolveDoc } from '../lib/feature-docs.mjs';
17
18
  import { planBoardMoves, applyBoardMoves } from '../lib/implement-board.mjs';
18
19
  import { extractPathsFromPlan, buildCodeDigest } from '../lib/code-digest.mjs';
19
20
 
@@ -41,15 +42,16 @@ async function resolveFeature(token, startNodeId) {
41
42
  return null;
42
43
  }
43
44
 
44
- // Lê spec.md/plan.md de um docs/features/<slug> se existirem.
45
+ // Lê os documentos ATUAIS (maior versão) de um docs/features/<slug>: com
46
+ // regerações forçadas o vigente é spec-v2.md/plan-v3.md…, não o original.
45
47
  function readSpecPlan(featureDir) {
46
- const specPath = path.join(featureDir, 'spec.md');
47
- const planPath = path.join(featureDir, 'plan.md');
48
+ const spec = resolveDoc(featureDir, 'spec');
49
+ const plan = resolveDoc(featureDir, 'plan');
48
50
  return {
49
- specPath,
50
- planPath,
51
- spec: existsSync(specPath) ? readFileSync(specPath, 'utf-8') : null,
52
- plan: existsSync(planPath) ? readFileSync(planPath, 'utf-8') : null,
51
+ specPath: spec?.path || path.join(featureDir, 'spec.md'),
52
+ planPath: plan?.path || path.join(featureDir, 'plan.md'),
53
+ spec: spec ? readFileSync(spec.path, 'utf-8') : null,
54
+ plan: plan ? readFileSync(plan.path, 'utf-8') : null,
53
55
  };
54
56
  }
55
57
 
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { resolveToken } from '../api/auth.mjs';
4
4
  import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
5
5
  import { slugify } from '../lib/slugify.mjs';
6
+ import { resolveDoc } from '../lib/feature-docs.mjs';
6
7
  import { CONFIG_FILE, LABEL_CRITIQUE_FAILED, REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS } from '../config.mjs';
7
8
  import { findIncompleteDocSigns } from '../lib/doc-completeness.mjs';
8
9
 
@@ -39,37 +40,39 @@ export async function validate({ issueNumber }) {
39
40
  );
40
41
  }
41
42
 
42
- // Check plan.md
43
- const planPath = `${featureDir}/plan.md`;
44
- if (!existsSync(planPath)) {
43
+ // Valida sempre o documento ATUAL de cada tipo — com regerações forçadas a
44
+ // versão vigente é a maior (plan-v2.md, plan-v3.md…), não o plan.md original.
45
+ const plan = resolveDoc(featureDir, 'plan');
46
+ const planPath = plan?.path || `${featureDir}/plan.md`;
47
+ if (!plan) {
45
48
  errors.push('❌ `plan.md` não encontrado em `' + planPath + '`');
46
49
  } else {
47
- const planContent = readFileSync(planPath, 'utf-8');
50
+ const planContent = readFileSync(plan.path, 'utf-8');
48
51
  for (const section of REQUIRED_PLAN_SECTIONS) {
49
52
  if (!planContent.includes(`# ${section}`)) {
50
- errors.push(`❌ Seção obrigatória ausente no plan.md: **${section}**`);
53
+ errors.push(`❌ Seção obrigatória ausente no plano (v${plan.version}): **${section}**`);
51
54
  }
52
55
  }
53
56
  for (const problem of findIncompleteDocSigns(planContent)) {
54
- errors.push(`❌ \`plan.md\` parece incompleto: ${problem}`);
57
+ errors.push(`❌ \`${planPath}\` parece incompleto: ${problem}`);
55
58
  }
56
59
  }
57
60
 
58
- // Check spec.md
59
- const specPath = `${featureDir}/spec.md`;
60
- if (!existsSync(specPath)) {
61
+ const spec = resolveDoc(featureDir, 'spec');
62
+ const specPath = spec?.path || `${featureDir}/spec.md`;
63
+ if (!spec) {
61
64
  errors.push('❌ `spec.md` não encontrado em `' + specPath + '`');
62
65
  } else {
63
- const specContent = readFileSync(specPath, 'utf-8');
66
+ const specContent = readFileSync(spec.path, 'utf-8');
64
67
  for (const section of REQUIRED_SPEC_SECTIONS) {
65
68
  if (!specContent.includes(`# ${section}`)) {
66
- errors.push(`❌ Seção obrigatória ausente no spec.md: **${section}**`);
69
+ errors.push(`❌ Seção obrigatória ausente na spec (v${spec.version}): **${section}**`);
67
70
  }
68
71
  }
69
72
  // Seções presentes não garantem documento completo: um corte dentro da
70
73
  // última seção passa na checagem acima (foi o caso da EP2-F13).
71
74
  for (const problem of findIncompleteDocSigns(specContent)) {
72
- errors.push(`❌ \`spec.md\` parece incompleto: ${problem}`);
75
+ errors.push(`❌ \`${specPath}\` parece incompleto: ${problem}`);
73
76
  }
74
77
  }
75
78
 
@@ -0,0 +1,89 @@
1
+ // Resolução dos documentos versionados de uma Feature (docs/features/<slug>/).
2
+ //
3
+ // A primeira geração escreve `spec.md`/`plan.md` (= v1). Cada regeração
4
+ // FORÇADA (flag --force ou label spec-wave:force) cria a próxima versão em vez
5
+ // de sobrescrever: `spec-v2.md`, `spec-v3.md`, … O documento ATUAL é sempre a
6
+ // MAIOR versão — é ele que validate/generate-plan/decompose/implement leem, e
7
+ // é ele que uma regeração sem force sobrescreve.
8
+ import { existsSync, readdirSync } from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ /**
12
+ * Versão de um arquivo de documento (função PURA).
13
+ * `spec.md` → 1; `spec-v2.md` → 2. Outro nome → null.
14
+ *
15
+ * @param {string} filename nome do arquivo (sem diretório)
16
+ * @param {string} base 'spec' ou 'plan'
17
+ * @returns {number|null}
18
+ */
19
+ export function parseDocVersion(filename, base) {
20
+ const match = new RegExp(`^${base}(?:-v(\\d+))?\\.md$`).exec(String(filename ?? ''));
21
+ if (!match) return null;
22
+ return match[1] ? parseInt(match[1], 10) : 1;
23
+ }
24
+
25
+ /**
26
+ * Documento ATUAL entre os arquivos dados: o de maior versão (função PURA).
27
+ * Empate (raro: `spec.md` e `spec-v1.md` coexistindo) resolve pelo sufixado,
28
+ * para o resultado ser determinístico.
29
+ *
30
+ * @param {string[]} filenames
31
+ * @param {string} base 'spec' ou 'plan'
32
+ * @returns {{ file: string, version: number }|null}
33
+ */
34
+ export function pickLatestDoc(filenames, base) {
35
+ const found = (filenames || [])
36
+ .map(file => ({ file, version: parseDocVersion(file, base), suffixed: /-v\d+\.md$/.test(file) }))
37
+ .filter(entry => entry.version !== null)
38
+ .sort((a, b) => (a.version - b.version) || (Number(a.suffixed) - Number(b.suffixed)));
39
+ if (found.length === 0) return null;
40
+ const { file, version } = found[found.length - 1];
41
+ return { file, version };
42
+ }
43
+
44
+ /**
45
+ * Nome do arquivo da PRÓXIMA versão (função PURA): `spec.md` quando ainda não
46
+ * há nenhum, `spec-v<N+1>.md` a partir da maior versão existente.
47
+ *
48
+ * @param {string[]} filenames
49
+ * @param {string} base 'spec' ou 'plan'
50
+ * @returns {string}
51
+ */
52
+ export function nextDocName(filenames, base) {
53
+ const latest = pickLatestDoc(filenames, base);
54
+ return latest ? `${base}-v${latest.version + 1}.md` : `${base}.md`;
55
+ }
56
+
57
+ // Nomes de arquivo do diretório da feature ([] se ele ainda não existe).
58
+ function readDir(featureDir) {
59
+ return featureDir && existsSync(featureDir) ? readdirSync(featureDir) : [];
60
+ }
61
+
62
+ /**
63
+ * Caminho do documento ATUAL (maior versão) — null se nenhum existe.
64
+ *
65
+ * @returns {{ path: string, version: number }|null}
66
+ */
67
+ export function resolveDoc(featureDir, base) {
68
+ const latest = pickLatestDoc(readDir(featureDir), base);
69
+ return latest ? { path: path.join(featureDir, latest.file), version: latest.version } : null;
70
+ }
71
+
72
+ /**
73
+ * Caminho onde a geração deve escrever.
74
+ * - `force: true` → próxima versão (nunca sobrescreve nada);
75
+ * - `force: false` → o documento atual (sobrescreve), ou `<base>.md` no 1º run.
76
+ *
77
+ * @returns {{ path: string, version: number, isNewVersion: boolean }}
78
+ */
79
+ export function resolveWritePath(featureDir, base, { force = false } = {}) {
80
+ const files = readDir(featureDir);
81
+ if (!force) {
82
+ const latest = pickLatestDoc(files, base);
83
+ return latest
84
+ ? { path: path.join(featureDir, latest.file), version: latest.version, isNewVersion: false }
85
+ : { path: path.join(featureDir, `${base}.md`), version: 1, isNewVersion: false };
86
+ }
87
+ const name = nextDocName(files, base);
88
+ return { path: path.join(featureDir, name), version: parseDocVersion(name, base), isNewVersion: true };
89
+ }
@@ -109,7 +109,11 @@ Label **modificadora** — `spec-wave:force`: sozinha **não dispara nada**; adi
109
109
  ```bash
110
110
  gh issue edit <n> --add-label "spec-wave:force" --add-label "spec-wave:decompose"
111
111
  ```
112
- Efeito por comando: no **decompose**, ignora o guard e **fecha as sub-issues da decomposição anterior** antes de gerar as novas (veja *Guard de idempotência*); em **spec** e **plan**, não muda nada na prática — eles já regeram e sobrescrevem o arquivo a cada acionamento da label.
112
+ Efeito por comando:
113
+ - **spec** e **plan** → geram a **próxima versão** em vez de sobrescrever: `spec.md` (v1) → `spec-v2.md` → `spec-v3.md`… As versões anteriores ficam intactas e a **maior versão passa a ser o documento atual** (veja *Documentos versionados*).
114
+ - **decompose** → ignora o guard e **fecha as sub-issues da decomposição anterior** antes de gerar as novas (veja *Guard de idempotência*).
115
+
116
+ Sem a label, `spec`/`plan` **sobrescrevem o documento atual** (comportamento de sempre).
113
117
 
114
118
  A etapa **🚧 Desenvolvimento** é coberta pelo comando **local** `npx @spec-wave/cli@latest implement <número>` (não é uma label/Action): lê uma **Feature** (todas as Stories pendentes, em ordem de dependência), uma Story ou uma Task e aciona o spec-kit para implementar. Veja `/spec-wave implement`.
115
119
 
@@ -246,6 +250,22 @@ Após o `generate-plan` e **antes** da criação de issues no `decompose`, um se
246
250
  - **Fluxo de resolução:** (1) leia o comentário 🔎 na issue; (2) corrija `spec.md`/`plan.md` (regenere com as labels ou edite e commite); (3) remova a label: `gh issue edit <n> --remove-label "spec-wave:critique-failed"`; (4) re-aplique `spec-wave:ready` para validar de novo.
247
251
  - Findings leves não bloqueiam — trate-os como revisão de qualidade.
248
252
 
253
+ ### Documentos versionados (`spec-v2.md`, `plan-v3.md`…)
254
+
255
+ A primeira geração escreve `spec.md` e `plan.md` — a **v1**. Cada regeração **forçada** (label `spec-wave:force`) grava a **próxima versão** ao lado, sem tocar nas anteriores:
256
+
257
+ ```
258
+ docs/features/<slug>/
259
+ spec.md ← v1
260
+ spec-v2.md ← 1ª regeração forçada
261
+ spec-v3.md ← 2ª regeração forçada ⬅ spec ATUAL
262
+ plan.md ⬅ plano ATUAL (spec e plan versionam independente)
263
+ ```
264
+
265
+ **A maior versão é sempre o documento atual** — é ela que o `generate-plan` usa como contexto, que o `validate` valida, que o `decompose` lê e que o `implement` anexa. Uma regeração **sem** force sobrescreve essa versão atual (não volta para o `spec.md` original).
266
+
267
+ Ao mostrar os documentos ao usuário, use a versão atual; ofereça as anteriores só se ele quiser comparar. Nunca renomeie nem apague versões antigas por conta própria — elas são o histórico da Feature.
268
+
249
269
  ### Guard de idempotência do decompose (`spec-wave:decomposed`)
250
270
 
251
271
  O evento `labeled` pode redisparar (re-add da label, retry de runner). Para não duplicar Stories/Tasks, o `decompose` **pula** quando a issue já tem a label **`spec-wave:decomposed`** ou já tem sub-issues do tipo-alvo (`[STORY]` para Feature, `[TASK]` para RFC). Ao concluir com sucesso, o Action grava a label. Os workflows ainda usam `concurrency` por issue para serializar runs simultâneos.
@@ -416,7 +436,10 @@ Inicia a geração da **especificação funcional** para uma Feature. É o **pri
416
436
  ```
417
437
  3. Informe: "Label `spec-wave:spec` adicionada. O GitHub Action `generate-spec.yml` irá gerar o `spec.md` automaticamente."
418
438
  4. Após a conclusão, ofereça revisar o spec.md gerado em `docs/features/<slug>/spec.md`.
419
- 5. **Para regerar** (a spec já existe e o usuário quer outra versão): basta re-adicionar a label — o Action sobrescreve o arquivo. A label `spec-wave:force` é aceita, mas aqui não muda nada; **avise que o spec.md atual será perdido** (inclusive edições manuais) e confirme antes.
439
+ 5. **Para regerar** (a spec já existe e o usuário quer outra versão): adicione `spec-wave:force` junto com o gatilho — o Action grava a **próxima versão** (`spec-v2.md`, `spec-v3.md`…) e preserva as anteriores; a nova passa a ser a spec atual. Sem o force, a versão atual é **sobrescrita** (edições manuais se perdem) avise antes de acionar assim.
440
+ ```bash
441
+ gh issue edit <número> --add-label "spec-wave:force" --add-label "spec-wave:spec"
442
+ ```
420
443
  6. Próximo passo: gerar o plano técnico — mova para **📋 Plan** e use `/spec-wave plan <número>`.
421
444
 
422
445
  ---
@@ -436,7 +459,7 @@ O plano técnico segue o schema do RFC-002 §3.2: **Estratégia Técnica** (com
436
459
  ```
437
460
  4. Informe: "Label `spec-wave:plan` adicionada. O GitHub Action `generate-plan.yml` irá gerar o `plan.md` automaticamente. Acompanhe em: Actions → Generate Plan."
438
461
  5. Após a conclusão (cheque comentários na issue ou aguarde confirmação do usuário), ofereça revisar o plan.md gerado em `docs/features/<slug>/plan.md`.
439
- 6. **Para regerar**: re-adicione a label o Action sobrescreve o `plan.md` (mesma ressalva do spec: edições manuais se perdem, confirme antes).
462
+ 6. **Para regerar**: adicione `spec-wave:force` junto com `spec-wave:plan` grava `plan-v2.md`, `plan-v3.md`… preservando as anteriores. Sem o force, o plano atual é sobrescrito (confirme antes).
440
463
  7. Próximo passo: validar a Feature — mova para **✅ Ready** e use `/spec-wave ready <número>`.
441
464
 
442
465
  ---
@@ -695,8 +718,12 @@ Audita um Pull Request e corrige automaticamente os problemas encontrados — se
695
718
  docs/
696
719
  features/
697
720
  <slug-da-feature>/
698
- spec.md ← gerado pelo GitHub Action quando spec-wave:spec é adicionado (1º)
699
- plan.md ← gerado pelo GitHub Action quando spec-wave:plan é adicionado (2º, usa a spec)
721
+ spec.md ← gerado quando spec-wave:spec é adicionado (1º) = v1
722
+ spec-v2.md ← regeração forçada (spec-wave:force); v1 preservada
723
+ plan.md ← gerado quando spec-wave:plan é adicionado (2º, usa a spec ATUAL) = v1
724
+ plan-v2.md ← regeração forçada do plano
700
725
  ```
701
726
 
727
+ A **maior versão de cada tipo é o documento atual** (aqui: `spec-v2.md` e `plan-v2.md`) — veja *Documentos versionados*.
728
+
702
729
  O slug é gerado a partir do título da issue: `[FEATURE] Cadastro de Pedidos com PIX` → `cadastro-de-pedidos-com-pix`