@spec-wave/cli 0.7.1 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
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,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
2
  import { resolveToken } from '../api/auth.mjs';
4
3
  import { getIssue, createIssue, removeLabel, addLabel, commentOnIssue, addBlockedBy } from '../api/github-rest.mjs';
5
- import { addSubIssue, addProjectItem, setItemSingleSelect, getSingleSelectField, listSubIssues } from '../api/github-graphql.mjs';
4
+ import { addSubIssue, listSubIssues } from '../api/github-graphql.mjs';
5
+ import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
6
6
  import { generateDocument } from '../lib/claude.mjs';
7
7
  import { runCritique } from '../lib/critique.mjs';
8
8
  import { recordUsage } from '../lib/usage-report.mjs';
@@ -10,34 +10,13 @@ import { formatDependencyLine } from '../lib/dependencies.mjs';
10
10
  import { lintLanguage } from '../lib/output-lint.mjs';
11
11
  import { slugify } from '../lib/slugify.mjs';
12
12
  import { detectIssueType } from '../lib/issue-type.mjs';
13
- import { CONFIG_FILE, DECOMPOSE_TARGETS, LABEL_DECOMPOSED, LABEL_CRITIQUE_FAILED, TARGET_LANGUAGE } from '../config.mjs';
13
+ import { DECOMPOSE_TARGETS, LABEL_DECOMPOSED, LABEL_CRITIQUE_FAILED, TARGET_LANGUAGE, STAGE_READY, PROGRESS_TODO } from '../config.mjs';
14
14
 
15
- const READY_STAGE = 'Todo';
16
-
17
- // Carrega o projeto do .spec-wave.json. Retorna null se ausente ou sem project.id.
18
- function loadProject() {
19
- const configPath = path.join(process.cwd(), CONFIG_FILE);
20
- if (!existsSync(configPath)) return null;
21
- try {
22
- return JSON.parse(readFileSync(configPath, 'utf-8')).project || null;
23
- } catch {
24
- return null;
25
- }
26
- }
27
-
28
- // Resolve o campo Status do Project: usa .spec-wave.json ou consulta API.
29
- async function resolveStatusField(token, project) {
30
- if (project.fields?.Status) return project.fields.Status;
31
- return await getSingleSelectField(token, project.id, 'Status');
32
- }
33
-
34
- // Adiciona issue ao board e move para a etapa informada. Best-effort.
35
- async function moveToStage(token, project, statusField, nodeId, stageName) {
36
- if (!project?.id || !statusField) return;
37
- const optionId = statusField.options?.[stageName];
38
- if (!statusField.id || !optionId) return;
39
- const itemId = await addProjectItem(token, project.id, nodeId);
40
- await setItemSingleSelect(token, project.id, itemId, statusField.id, optionId);
15
+ // Adiciona a issue ao board na Etapa ✅ Ready / Status Todo. Best-effort; a
16
+ // Etapa nunca retrocede (advanceToStage não toca itens já adiante).
17
+ async function moveToReady(token, project, etapaField, statusField, nodeId) {
18
+ if (!project?.id) return;
19
+ await advanceToStage(token, project, etapaField, statusField, nodeId, STAGE_READY, PROGRESS_TODO);
41
20
  }
42
21
 
43
22
  // Extrai JSON da resposta do modelo (tolera texto em volta).
@@ -149,7 +128,7 @@ Regras:
149
128
 
150
129
  // Decompõe uma Feature em Stories (+ Tasks), cada uma vinculada como sub-issue.
151
130
  async function decomposeFeature(ctx) {
152
- const { token, projectToken, owner, repo, issue, issueNumber, project, statusField, usage } = ctx;
131
+ const { token, projectToken, owner, repo, issue, issueNumber, project, etapaField, statusField, usage } = ctx;
153
132
  const slug = slugify(issue.title);
154
133
  const featureDir = `docs/features/${slug}`;
155
134
 
@@ -243,9 +222,9 @@ async function decomposeFeature(ctx) {
243
222
  console.warn(` Story #${createdStory.number} criada, mas falhou ao vincular à Feature: ${err.message}`);
244
223
  }
245
224
  try {
246
- await moveToStage(projectToken, project, statusField, createdStory.nodeId, READY_STAGE);
225
+ await moveToReady(projectToken, project, etapaField, statusField, createdStory.nodeId);
247
226
  } catch (err) {
248
- console.warn(` Falha ao mover story #${createdStory.number} para "${READY_STAGE}": ${err.message}`);
227
+ console.warn(` Falha ao mover story #${createdStory.number} para "${STAGE_READY}": ${err.message}`);
249
228
  }
250
229
 
251
230
  for (const task of story.tasks || []) {
@@ -260,18 +239,18 @@ async function decomposeFeature(ctx) {
260
239
  console.warn(` Task #${createdTask.number} criada, mas falhou ao vincular à Story: ${err.message}`);
261
240
  }
262
241
  try {
263
- await moveToStage(projectToken, project, statusField, createdTask.nodeId, READY_STAGE);
242
+ await moveToReady(projectToken, project, etapaField, statusField, createdTask.nodeId);
264
243
  } catch (err) {
265
- console.warn(` Falha ao mover task #${createdTask.number} para "${READY_STAGE}": ${err.message}`);
244
+ console.warn(` Falha ao mover task #${createdTask.number} para "${STAGE_READY}": ${err.message}`);
266
245
  }
267
246
  }
268
247
  }
269
248
 
270
249
  try {
271
- await moveToStage(projectToken, project, statusField, featureNodeId, READY_STAGE);
272
- if (project?.id && statusField) console.log(`Feature movida para "${READY_STAGE}" no board.`);
250
+ await moveToReady(projectToken, project, etapaField, statusField, featureNodeId);
251
+ if (project?.id && etapaField) console.log(`Feature movida para "${STAGE_READY}" no board.`);
273
252
  } catch (err) {
274
- console.warn(`Falha ao mover Feature para "${READY_STAGE}": ${err.message}`);
253
+ console.warn(`Falha ao mover Feature para "${STAGE_READY}": ${err.message}`);
275
254
  }
276
255
 
277
256
  // Marca a Feature como decomposta (guard de idempotência em runs futuros).
@@ -295,7 +274,7 @@ async function decomposeFeature(ctx) {
295
274
  // Decompõe um RFC diretamente em Tasks (sem Stories), cada uma vinculada como
296
275
  // sub-issue do RFC.
297
276
  async function decomposeRFC(ctx) {
298
- const { token, projectToken, owner, repo, issue, issueNumber, project, statusField, usage } = ctx;
277
+ const { token, projectToken, owner, repo, issue, issueNumber, project, etapaField, statusField, usage } = ctx;
299
278
  console.log(`Decompondo RFC: ${issue.title}`);
300
279
 
301
280
  const userContent = [
@@ -324,9 +303,9 @@ async function decomposeRFC(ctx) {
324
303
  console.warn(` Task #${createdTask.number} criada, mas falhou ao vincular ao RFC: ${err.message}`);
325
304
  }
326
305
  try {
327
- await moveToStage(projectToken, project, statusField, createdTask.nodeId, READY_STAGE);
306
+ await moveToReady(projectToken, project, etapaField, statusField, createdTask.nodeId);
328
307
  } catch (err) {
329
- console.warn(` Falha ao mover task #${createdTask.number} para "${READY_STAGE}": ${err.message}`);
308
+ console.warn(` Falha ao mover task #${createdTask.number} para "${STAGE_READY}": ${err.message}`);
330
309
  }
331
310
  }
332
311
 
@@ -399,12 +378,19 @@ export async function decompose({ issueNumber }) {
399
378
  return;
400
379
  }
401
380
 
402
- // Projeto + campo Status (reutilizado em todos os itens).
403
- const project = loadProject();
381
+ // Projeto + campos Etapa/Status do board (reutilizados em todos os itens).
382
+ const { project, error: projectError } = loadProjectConfig();
383
+ if (projectError) console.warn(`${projectError} — itens criados não serão posicionados no board.`);
384
+ let etapaField = null;
404
385
  let statusField = null;
405
386
  if (project?.id) {
406
387
  try {
407
- statusField = await resolveStatusField(projectToken, project);
388
+ etapaField = await resolveField(projectToken, project, 'Etapa');
389
+ } catch (err) {
390
+ console.warn(`Não foi possível resolver campo Etapa do board: ${err.message}`);
391
+ }
392
+ try {
393
+ statusField = await resolveField(projectToken, project, 'Status');
408
394
  } catch (err) {
409
395
  console.warn(`Não foi possível resolver campo Status do board: ${err.message}`);
410
396
  }
@@ -413,7 +399,7 @@ export async function decompose({ issueNumber }) {
413
399
  // Coletor de uso de IA — o finally registra o custo já incorrido mesmo nos
414
400
  // fluxos que retornam cedo (ex.: abort da crítica grave) ou que falham.
415
401
  const usageEntries = [];
416
- const ctx = { token, projectToken, owner, repo, issue, issueNumber, project, statusField, usage: usageEntries };
402
+ const ctx = { token, projectToken, owner, repo, issue, issueNumber, project, etapaField, statusField, usage: usageEntries };
417
403
  try {
418
404
  if (type === 'Feature') await decomposeFeature(ctx);
419
405
  else if (type === 'RFC') await decomposeRFC(ctx);
@@ -333,6 +333,37 @@ async function checkAi(ctx) {
333
333
  return { name, status, detail: notes.join('\n') };
334
334
  }
335
335
 
336
+ // Exportado para teste: só lê ctx.cfg e process.env — sem rede/filesystem.
337
+ export function checkSpecKit(ctx) {
338
+ const name = 'Spec-kit (specKit.command para o implement)';
339
+ const fromEnv = process.env.SPEC_WAVE_IMPLEMENT_CMD;
340
+ const fromConfig = ctx.cfg?.specKit?.command;
341
+ if (fromEnv) {
342
+ return {
343
+ name,
344
+ status: 'ok',
345
+ detail: `Definido via env SPEC_WAVE_IMPLEMENT_CMD${fromConfig ? ' (sobrepõe o specKit.command do config)' : ''}: ${fromEnv}`,
346
+ };
347
+ }
348
+ if (fromConfig) {
349
+ return { name, status: 'ok', detail: `Definido no ${CONFIG_FILE}: ${fromConfig}` };
350
+ }
351
+ return {
352
+ name,
353
+ status: 'warn',
354
+ detail:
355
+ 'Nenhum comando configurado — `implement` só monta o contexto, sem acionar um agente.\n' +
356
+ `Defina "specKit": { "command": "..." } no ${CONFIG_FILE} (ou a env SPEC_WAVE_IMPLEMENT_CMD).\n` +
357
+ 'Placeholders: {tasksFile} {specFile} {planFile} {issue} {type} {title}. Exemplos por agente:\n' +
358
+ ' Claude Code: claude -p "Implemente as tasks descritas em {tasksFile}"\n' +
359
+ ' opencode: opencode run "Implemente as tasks descritas em {tasksFile}"\n' +
360
+ ' Codex: codex exec "Implemente as tasks descritas em {tasksFile}"\n' +
361
+ ' Copilot CLI: copilot -p "Implemente as tasks descritas em {tasksFile}" --allow-all-tools\n' +
362
+ ' Kiro CLI: kiro-cli chat --no-interactive --trust-all-tools "Implemente as tasks descritas em {tasksFile}"\n' +
363
+ ' Qwen Code: qwen -p "Implemente as tasks descritas em {tasksFile}"',
364
+ };
365
+ }
366
+
336
367
  async function checkWorkflows(ctx) {
337
368
  const name = 'Workflows do Actions';
338
369
  const dir = path.join(ctx.cwd, '.github', 'workflows');
@@ -380,6 +411,7 @@ export async function doctor() {
380
411
  checkConfig,
381
412
  checkRepoAccess,
382
413
  checkAi,
414
+ checkSpecKit,
383
415
  checkWorkflows,
384
416
  ];
385
417
  const results = [];
@@ -27,6 +27,7 @@ O plano deve conter EXATAMENTE estas seções em português, nesta ordem:
27
27
  # Estratégia Técnica
28
28
  - Abordagem Arquitetural, Decisões-Chave e uma Matriz de Rastreabilidade (tabela) ligando cada Critério de Aceite do spec a um componente técnico.
29
29
  # Detalhamento da Implementação
30
+ - Abra a seção com um diagrama de sequência Mermaid (bloco \`\`\`mermaid iniciado com sequenceDiagram) do fluxo principal ponta a ponta, com os componentes técnicos reais como participants (frontend, endpoints/controllers, services, banco de dados, filas). Use APENAS componentes do tech_context ou definidos neste plano; rotule as mensagens com os caminhos de endpoint e nomes de método reais, em português.
30
31
  - Subseções: ## Backend, ## Banco de Dados, ## Frontend, ## Infraestrutura.
31
32
  # Segurança e Conformidade
32
33
  # Estratégia de Testes
@@ -27,6 +27,8 @@ O spec deve conter EXATAMENTE estas seções em português, nesta ordem:
27
27
  # Regras de Negócio
28
28
  # Fluxos
29
29
  - Subseções: ## Fluxo Principal (Happy Path), ## Fluxos Alternativos, ## Cenários de Erro.
30
+ - O Fluxo Principal DEVE conter, além da descrição passo a passo, um diagrama de sequência Mermaid (bloco \`\`\`mermaid iniciado com sequenceDiagram) mostrando a interação entre as personas (actor) e o sistema (participant). Rotule mensagens e notas em português.
31
+ - Cubra os Fluxos Alternativos e Cenários de Erro relevantes no mesmo diagrama usando blocos alt/opt/break — ou, se ficarem complexos, em um segundo diagrama na subseção correspondente.
30
32
  # Critérios de Aceite
31
33
  - OBRIGATORIAMENTE no formato Gherkin, dentro de um bloco \`\`\`gherkin com Given/When/Then. Um cenário por critério.
32
34
  # Dependências
@@ -3,20 +3,57 @@ import chalk from 'chalk';
3
3
  import { readFileSync, existsSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { CONFIG_FILE, PORTAL_URL } from '../config.mjs';
6
+ import { skillStatus } from './install-skill.mjs';
7
+
8
+ // Resume o estado da skill para a saída JSON. `null` = não foi possível checar.
9
+ function skillJson(status) {
10
+ if (!status) return null;
11
+ return {
12
+ agentsDetected: status.agentsDetected,
13
+ installNeeded: status.agentsDetected.length === 0 || status.pending.length > 0,
14
+ pending: status.pending,
15
+ };
16
+ }
17
+
18
+ // Reporta (saída humana) se o usuário precisa rodar o install-skill: nenhum
19
+ // agente detectado → não dá para validar, sugere instalar; cópia ausente ou
20
+ // de versão antiga → aponta o agente e o motivo.
21
+ function reportSkill(status) {
22
+ if (!status) return;
23
+ if (status.agentsDetected.length === 0) {
24
+ p.log.warn(
25
+ 'Nenhum agente de código detectado neste diretório — a skill spec-wave ' +
26
+ 'não parece instalada. Rode `npx @spec-wave/cli install-skill`.'
27
+ );
28
+ return;
29
+ }
30
+ if (status.pending.length === 0) {
31
+ p.log.success(`Skill instalada e atualizada (${status.agentsDetected.join(', ')}).`);
32
+ return;
33
+ }
34
+ p.log.warn(
35
+ 'Skill pendente de instalação/atualização:\n' +
36
+ status.pending.map(s => ` ${chalk.yellow('↻')} ${s.agent} (${s.reason}) — ${chalk.dim(s.path)}`).join('\n') +
37
+ '\nRode `npx @spec-wave/cli install-skill` (ou `npx @spec-wave/cli update`).'
38
+ );
39
+ }
6
40
 
7
41
  // Lê o marcador .spec-wave.json do repositório atual (cwd) e reporta se o
8
42
  // spec-wave já foi inicializado. Usado pela skill para decidir entre mostrar
9
- // as informações ou oferecer rodar o `init`.
43
+ // as informações ou oferecer rodar o `init`. Também valida se a skill instalada
44
+ // nos agentes detectados está em dia com a versão empacotada na CLI.
10
45
  export async function info(options = {}) {
11
46
  const configPath = path.join(process.cwd(), CONFIG_FILE);
47
+ const skill = skillStatus();
12
48
 
13
49
  if (!existsSync(configPath)) {
14
50
  if (options.json) {
15
- console.log(JSON.stringify({ initialized: false }));
51
+ console.log(JSON.stringify({ initialized: false, skill: skillJson(skill) }));
16
52
  return;
17
53
  }
18
54
  p.intro(chalk.bold('spec-wave info'));
19
55
  p.log.warn(`Este repositório ${chalk.bold('não foi inicializado')} (sem ${CONFIG_FILE}).`);
56
+ reportSkill(skill);
20
57
  p.log.info(`🌐 Acesse o Portal Web da ferramenta em ${chalk.cyan(PORTAL_URL)}`);
21
58
  p.outro('Execute `npx @spec-wave/cli init` para configurar.');
22
59
  return;
@@ -27,7 +64,7 @@ export async function info(options = {}) {
27
64
  config = JSON.parse(readFileSync(configPath, 'utf-8'));
28
65
  } catch (err) {
29
66
  if (options.json) {
30
- console.log(JSON.stringify({ initialized: false, error: err.message }));
67
+ console.log(JSON.stringify({ initialized: false, error: err.message, skill: skillJson(skill) }));
31
68
  return;
32
69
  }
33
70
  p.log.error(`${CONFIG_FILE} existe mas está corrompido: ${err.message}`);
@@ -36,7 +73,7 @@ export async function info(options = {}) {
36
73
  }
37
74
 
38
75
  if (options.json) {
39
- console.log(JSON.stringify({ initialized: true, ...config }));
76
+ console.log(JSON.stringify({ initialized: true, ...config, skill: skillJson(skill) }));
40
77
  return;
41
78
  }
42
79
 
@@ -51,6 +88,7 @@ export async function info(options = {}) {
51
88
  `${chalk.dim('Criado em:')} ${config.initializedAt ?? '?'}`,
52
89
  'Configuração'
53
90
  );
91
+ reportSkill(skill);
54
92
  p.log.info(`🌐 Acesse o Portal Web da ferramenta em ${chalk.cyan(PORTAL_URL)}`);
55
93
  p.outro('Use `/spec-wave feature <descrição>` para criar uma Feature.');
56
94
  }
@@ -178,6 +178,46 @@ export function isDetected(target, baseDir) {
178
178
  return target.detect.some((sig) => existsSync(path.join(baseDir, sig)));
179
179
  }
180
180
 
181
+ // Motivo pelo qual a cópia da skill em `dest` precisa ser (re)instalada:
182
+ // 'ausente' | 'bloco ausente' | 'desatualizada' — ou null se está em dia com a
183
+ // versão empacotada na CLI. Compartilhado entre `update` e `info`.
184
+ export function skillCopyReason(dest, parsed) {
185
+ const desired = renderContent(dest.format, parsed, CLI_VERSION);
186
+ const existing = existsSync(dest.path) ? readFileSync(dest.path, 'utf-8') : null;
187
+ if (existing === null) return 'ausente';
188
+ if (dest.format === 'agents') {
189
+ const block = extractAgentsBlock(existing);
190
+ if (block === null) return 'bloco ausente';
191
+ return block.trim() !== desired.trim() ? 'desatualizada' : null;
192
+ }
193
+ return existing !== desired ? 'desatualizada' : null;
194
+ }
195
+
196
+ // Estado da skill para os agentes detectados em cwd (escopo projeto), com
197
+ // fallback: uma cópia GLOBAL atualizada atende o agente mesmo sem cópia local.
198
+ // Retorna null quando a fonte da skill não está no pacote (instalação parcial).
199
+ export function skillStatus(cwd = process.cwd()) {
200
+ if (!existsSync(SKILL_SOURCE)) return null;
201
+ const parsed = parseSkill(readFileSync(SKILL_SOURCE, 'utf-8'));
202
+ const agentsDetected = [];
203
+ const pending = [];
204
+ for (const target of TARGETS) {
205
+ if (!isDetected(target, cwd)) continue;
206
+ agentsDetected.push(target.name);
207
+ const dest = resolveDest(target, cwd, false);
208
+ if (!dest) continue;
209
+ let reason = skillCopyReason(dest, parsed);
210
+ if (reason === 'ausente') {
211
+ const globalDest = resolveDest(target, homedir(), true);
212
+ if (globalDest && skillCopyReason(globalDest, parsed) === null) reason = null;
213
+ }
214
+ if (reason) {
215
+ pending.push({ agent: target.name, key: target.key, path: dest.path, reason });
216
+ }
217
+ }
218
+ return { agentsDetected, pending };
219
+ }
220
+
181
221
  export async function installSkill(options = {}) {
182
222
  p.intro(chalk.bold('spec-wave install-skill'));
183
223
 
@@ -12,7 +12,7 @@ import {
12
12
  } from '../api/github-rest.mjs';
13
13
  import {
14
14
  TARGETS, SKILL_SOURCE, CLI_VERSION, parseSkill, renderContent,
15
- mergeAgentsFile, resolveDest, isDetected, extractAgentsBlock,
15
+ mergeAgentsFile, resolveDest, isDetected, skillCopyReason,
16
16
  } from './install-skill.mjs';
17
17
 
18
18
  const __dir = path.dirname(fileURLToPath(import.meta.url));
@@ -35,19 +35,10 @@ function detectSkill(parsed, baseDir, isGlobal) {
35
35
  if (!isDetected(target, baseDir)) continue;
36
36
  const dest = resolveDest(target, baseDir, isGlobal);
37
37
  if (!dest) continue;
38
- const desired = renderContent(dest.format, parsed, CLI_VERSION);
39
- const existing = existsSync(dest.path) ? readFileSync(dest.path, 'utf-8') : null;
40
- let reason = null;
41
- if (existing === null) {
42
- reason = 'ausente';
43
- } else if (dest.format === 'agents') {
44
- const block = extractAgentsBlock(existing);
45
- if (block === null) reason = 'bloco ausente';
46
- else if (block.trim() !== desired.trim()) reason = 'desatualizada';
47
- } else if (existing !== desired) {
48
- reason = 'desatualizada';
38
+ const reason = skillCopyReason(dest, parsed);
39
+ if (reason) {
40
+ jobs.push({ target, dest, desired: renderContent(dest.format, parsed, CLI_VERSION), reason });
49
41
  }
50
- if (reason) jobs.push({ target, dest, desired, reason });
51
42
  }
52
43
  return jobs;
53
44
  }
package/src/config.mjs CHANGED
@@ -68,6 +68,7 @@ export const STATUS_OPTIONS = [
68
68
  // atual. Ao avançar de etapa, o Status reinicia em "Todo".
69
69
 
70
70
  // Etapas (campo Etapa) referenciadas pelo fluxo de implementação.
71
+ export const STAGE_READY = STATUS_OPTIONS.find(s => s.name.includes('Ready')).name;
71
72
  export const STAGE_DEVELOPMENT = STATUS_OPTIONS.find(s => s.name.includes('Desenvolvimento')).name;
72
73
  export const STAGE_CODE_REVIEW = STATUS_OPTIONS.find(s => s.name.includes('Code Review')).name;
73
74
  export const STAGE_DONE = STATUS_OPTIONS.find(s => s.name.includes('Done')).name;
@@ -137,7 +137,9 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
137
137
  ### `@spec-wave/cli info` — status de configuração do repo atual
138
138
  | Flag | Tipo | Descrição |
139
139
  |------|------|-----------|
140
- | `--json` | flag | Saída JSON (`{"initialized":bool, ...}`) para parsing programático. |
140
+ | `--json` | flag | Saída JSON (`{"initialized":bool, ..., "skill":{...}}`) para parsing programático. |
141
+
142
+ > Além do `.spec-wave.json`, valida a **skill instalada**: para cada agente detectado no diretório, compara a cópia instalada com a versão empacotada na CLI (uma cópia **global** atualizada também conta). No JSON, o campo `skill` traz `{agentsDetected, installNeeded, pending:[{agent, reason, path}]}` — `installNeeded: true` significa que o usuário precisa rodar `install-skill` (ou `update`).
141
143
 
142
144
  ### `@spec-wave/cli refresh` — atualiza o `.spec-wave.json` local
143
145
  | Flag | Tipo | Descrição |
@@ -177,7 +179,7 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
177
179
  > Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Story** → coleta todas as Tasks (sub-issues) e aciona o spec-kit uma única vez; **Task** → só aquela task. Monta o contexto em `.spec-wave/implement-<n>.md` e chama o comando configurado em `specKit.command` (no `.spec-wave.json`) ou na env `SPEC_WAVE_IMPLEMENT_CMD`. Placeholders disponíveis no template: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`. Se nada estiver configurado, ele apenas monta o contexto e mostra como configurar (não executa). O contexto inclui os **comentários da issue**, um **digest do código recente** e um **aviso de dependências pendentes** quando a issue depende (linha `Depende de: #N` ou relação nativa *blocked by*) de outra que ainda não foi concluída — nesse caso, confirme com o usuário antes de seguir. Inclui também instruções para o agente implementar as Tasks **sequencialmente, uma por vez** (nunca duas com Status "In Progress" ao mesmo tempo): cada Task usa o **Status** (In Progress) *dentro* da Etapa 🚧 Desenvolvimento e, **ao concluir, avança para a Etapa 🎉 Done com Status Done**. **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa da Story para 👀 Code Review** (Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** para Code Review quando **TODAS as suas Stories** já estiverem em Code Review — enquanto houver Story pendente, a Feature fica em 🚧 Desenvolvimento. Etapa só avança (nunca volta); Status mede o progresso dentro da etapa.
178
180
 
179
181
  ### `@spec-wave/cli doctor` — preflight de auth e configuração (comando LOCAL)
180
- Sem flags. Roda um checklist de diagnóstico no repositório atual: token GitHub (e a fonte dele), escopos (`repo`, `project`, `workflow` — com degradação para checks funcionais em fine-grained PATs), conta ativa do `gh` vs. owner, `.spec-wave.json` (campos e sincronia com o Project real), acesso ao repositório, configuração de IA (provider/modelo/`ai.models` + secrets do Actions) e presença dos workflows.
182
+ Sem flags. Roda um checklist de diagnóstico no repositório atual: token GitHub (e a fonte dele), escopos (`repo`, `project`, `workflow` — com degradação para checks funcionais em fine-grained PATs), conta ativa do `gh` vs. owner, `.spec-wave.json` (campos e sincronia com o Project real), acesso ao repositório, configuração de IA (provider/modelo/`ai.models` + secrets do Actions), **spec-kit** (`specKit.command` / env `SPEC_WAVE_IMPLEMENT_CMD` — se ausente, avisa e sugere exemplos por agente: Claude Code, opencode, Codex, Copilot CLI, Kiro CLI, Qwen Code) e presença dos workflows.
181
183
 
182
184
  > Saída: `✓` ok, `✗` problema confirmado, `!` não verificável (best-effort — falha de rede nunca derruba o doctor). **Exit 1** se houver algum `✗`. **Quando rodar:** no início de uma sessão de trabalho, ou sempre que aparecer um erro estranho (ex.: **404 ao criar issues** — causa típica: token sem acesso ao repo/org, que o doctor aponta). É o primeiro passo de troubleshooting — prefira-o a depurar `gh api` na mão.
183
185
 
@@ -263,6 +265,7 @@ Mostra se o repositório atual já foi configurado com o spec-wave.
263
265
  3. **Se NÃO estiver inicializado**, pergunte ao usuário: "Este repositório ainda não foi configurado com o spec-wave. Quer rodar o `init` agora?"
264
266
  - Se sim → siga o fluxo de `/spec-wave setup`.
265
267
  - Se não → encerre sem alterar nada.
268
+ 4. **Se a saída indicar skill pendente** (aviso "Skill pendente de instalação/atualização" ou, no `--json`, `skill.installNeeded: true`), pergunte ao usuário se quer instalar/atualizar agora: skill `ausente` → `npx @spec-wave/cli install-skill`; skill `desatualizada` → `npx @spec-wave/cli update` (atualiza tudo que ficou para trás). Lembre-o de recarregar o agente depois.
266
269
 
267
270
  ---
268
271
 
@@ -470,7 +473,7 @@ Para qualquer outro tipo (Spike, Bug, Story, Task, …) o Action **recusa** e co
470
473
  gh issue edit <número> --add-label "spec-wave:decompose"
471
474
  ```
472
475
  3. Informe: "Decomposição iniciada — Feature gera Stories+Tasks; RFC gera Tasks."
473
- 4. Após a conclusão, as issues filhas aparecerão como comentário na issue pai, junto com o comentário 🔎 da crítica adversarial. As Stories geradas trazem a linha `Depende de: #N` (+ relação *blocked by*) — use `npx @spec-wave/cli order <número>` para ver a ordem de execução.
476
+ 4. Após a conclusão, as issues filhas aparecerão como comentário na issue pai, junto com o comentário 🔎 da crítica adversarial. A issue pai e as Stories/Tasks criadas entram no board na Etapa **✅ Ready** (Status Todo; a Etapa nunca retrocede — itens já adiante não são tocados). As Stories geradas trazem a linha `Depende de: #N` (+ relação *blocked by*) — use `npx @spec-wave/cli order <número>` para ver a ordem de execução.
474
477
  5. A issue recebe a label `spec-wave:decomposed` (guard de idempotência): rodar de novo **não** duplica as issues. Para forçar um re-decompose, siga a seção *Guard de idempotência*.
475
478
 
476
479
  ---