@spec-wave/cli 0.7.1 → 0.8.1
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 +1 -1
- package/bin/spec-wave.mjs +2 -2
- package/package.json +1 -1
- package/src/commands/decompose.mjs +30 -44
- package/src/commands/doctor.mjs +32 -0
- package/src/commands/generate-plan.mjs +1 -0
- package/src/commands/generate-spec.mjs +2 -0
- package/src/commands/implement.mjs +409 -70
- package/src/commands/info.mjs +42 -4
- package/src/commands/install-skill.mjs +40 -0
- package/src/commands/update.mjs +4 -13
- package/src/config.mjs +1 -0
- package/src/templates/skill/SKILL.md +19 -13
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Ferramenta Node.js que configura e opera o fluxo via linha de comando.
|
|
|
72
72
|
| `decompose` | Decompõe Feature em Stories e Tasks (usado pelo GitHub Action) |
|
|
73
73
|
| `code-review` | Move Feature para Code Review ao abrir PR (usado pelo GitHub Action) |
|
|
74
74
|
| `qa` | Move Feature para QA ao aprovar PR (usado pelo GitHub Action) |
|
|
75
|
-
| `implement` | Aciona o spec-kit localmente para implementar uma Story ou Task |
|
|
75
|
+
| `implement` | Aciona o spec-kit localmente para implementar uma Feature (Stories pendentes em ordem de dependência), Story ou Task |
|
|
76
76
|
| `uninstall` | Remove labels, workflows e `.spec-wave.json` |
|
|
77
77
|
|
|
78
78
|
### GitHub Actions (instalados pelo `init`)
|
package/bin/spec-wave.mjs
CHANGED
|
@@ -184,8 +184,8 @@ program
|
|
|
184
184
|
|
|
185
185
|
program
|
|
186
186
|
.command('implement')
|
|
187
|
-
.description('Aciona o spec-kit implement para uma Story (todas as tasks) ou uma Task')
|
|
188
|
-
.argument('<issue>', 'Número da issue (Story ou Task), ex.: 12 ou #12')
|
|
187
|
+
.description('Aciona o spec-kit implement para uma Feature (Stories pendentes em ordem de dependência), uma Story (todas as tasks) ou uma Task')
|
|
188
|
+
.argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
|
|
189
189
|
.option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
|
|
190
190
|
.option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
|
|
191
191
|
.action(async (issue, options) => {
|
package/package.json
CHANGED
|
@@ -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,
|
|
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 {
|
|
13
|
+
import { DECOMPOSE_TARGETS, LABEL_DECOMPOSED, LABEL_CRITIQUE_FAILED, TARGET_LANGUAGE, STAGE_READY, PROGRESS_TODO } from '../config.mjs';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
225
|
+
await moveToReady(projectToken, project, etapaField, statusField, createdStory.nodeId);
|
|
247
226
|
} catch (err) {
|
|
248
|
-
console.warn(` Falha ao mover story #${createdStory.number} para "${
|
|
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
|
|
242
|
+
await moveToReady(projectToken, project, etapaField, statusField, createdTask.nodeId);
|
|
264
243
|
} catch (err) {
|
|
265
|
-
console.warn(` Falha ao mover task #${createdTask.number} para "${
|
|
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
|
|
272
|
-
if (project?.id &&
|
|
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 "${
|
|
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
|
|
306
|
+
await moveToReady(projectToken, project, etapaField, statusField, createdTask.nodeId);
|
|
328
307
|
} catch (err) {
|
|
329
|
-
console.warn(` Falha ao mover task #${createdTask.number} para "${
|
|
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 +
|
|
403
|
-
const project =
|
|
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
|
-
|
|
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);
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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
|
|
@@ -5,19 +5,24 @@ import { execSync } from 'node:child_process';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveToken } from '../api/auth.mjs';
|
|
7
7
|
import {
|
|
8
|
-
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_DONE,
|
|
8
|
+
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_DONE, STAGE_ORDER,
|
|
9
9
|
PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE,
|
|
10
10
|
} from '../config.mjs';
|
|
11
11
|
import { getIssue, listIssueComments, listBlockedBy } from '../api/github-rest.mjs';
|
|
12
|
-
import { listSubIssues, getIssueParent } from '../api/github-graphql.mjs';
|
|
12
|
+
import { listSubIssues, getIssueParent, addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
13
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
14
14
|
import { slugify } from '../lib/slugify.mjs';
|
|
15
|
-
import { parseDependencies } from '../lib/dependencies.mjs';
|
|
15
|
+
import { parseDependencies, orderStories, formatDependencyLine } from '../lib/dependencies.mjs';
|
|
16
|
+
import { loadProjectConfig, resolveField } from '../lib/board.mjs';
|
|
16
17
|
import { extractPathsFromPlan, buildCodeDigest } from '../lib/code-digest.mjs';
|
|
17
18
|
|
|
18
19
|
// Diretório onde montamos o arquivo de contexto entregue ao spec-kit.
|
|
19
20
|
const WORK_DIR = '.spec-wave';
|
|
20
21
|
|
|
22
|
+
// Caps dos comentários anexados ao contexto (por issue).
|
|
23
|
+
const MAX_COMMENTS_PER_ISSUE = 15;
|
|
24
|
+
const MAX_COMMENT_CHARS = 2000;
|
|
25
|
+
|
|
21
26
|
// Sobe a cadeia de pais (Task → Story → Feature) até achar uma issue do tipo
|
|
22
27
|
// "Feature" e devolve { number, title } — usado para resolver docs/features/<slug>
|
|
23
28
|
// e para as instruções de fim de Story (mover a Feature para Code Review). Limita
|
|
@@ -47,6 +52,89 @@ function readSpecPlan(featureDir) {
|
|
|
47
52
|
};
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
// ── Blocos compartilhados entre buildContext (Story/Task) e buildFeatureContext ──
|
|
56
|
+
|
|
57
|
+
// Explica os dois campos do board (Etapa × Status) — abre as instruções de execução.
|
|
58
|
+
function boardFieldsExplainer() {
|
|
59
|
+
return (
|
|
60
|
+
'Há **dois campos** no board com papéis diferentes — não os confunda:\n' +
|
|
61
|
+
`- **Etapa** (Backlog → … → ${STAGE_DEVELOPMENT} → ${STAGE_CODE_REVIEW} → … → ${STAGE_DONE}): a DIREÇÃO no kanban. Uma issue só **avança**, **nunca** volta para uma etapa anterior.\n` +
|
|
62
|
+
`- **Status** (${PROGRESS_TODO} → ${PROGRESS_IN_PROGRESS} → ${PROGRESS_DONE}): o **progresso dentro da etapa atual**. Ao avançar de etapa, o Status reinicia em ${PROGRESS_TODO} — exceto ao chegar na Etapa ${STAGE_DONE}, onde o Status fica **${PROGRESS_DONE}**.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Blockquote-resumo da regra do board — fecha as instruções de execução.
|
|
67
|
+
function boardRuleBlockquote() {
|
|
68
|
+
return (
|
|
69
|
+
`> **Regra do board:** a **Etapa** só avança (nunca retrocede); o **Status** (${PROGRESS_TODO}/${PROGRESS_IN_PROGRESS}/${PROGRESS_DONE}) ` +
|
|
70
|
+
`mede o progresso dentro da etapa atual e reinicia a cada avanço (na Etapa ${STAGE_DONE}, o Status fica ${PROGRESS_DONE}).`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Dependências ainda abertas — logo após o cabeçalho, para máxima visibilidade.
|
|
75
|
+
function pushBlockedByWarnings(lines, blockedByWarnings) {
|
|
76
|
+
if (!blockedByWarnings || blockedByWarnings.length === 0) return;
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('## ⚠️ Dependências pendentes');
|
|
79
|
+
lines.push('');
|
|
80
|
+
for (const w of blockedByWarnings) lines.push(`- ${w}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(
|
|
83
|
+
'**Implemente somente se tiver certeza de que a dependência não é bloqueante; ' +
|
|
84
|
+
'caso contrário, pare e reporte.**'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Comentários das issues — é onde vivem as revisões/correções feitas depois
|
|
89
|
+
// que spec/plan/stories foram escritos; em conflito, o comentário vence.
|
|
90
|
+
function pushCommentsSection(lines, comments) {
|
|
91
|
+
if (!comments || comments.length === 0) return;
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('## Comentários das issues (revisões e correções)');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(
|
|
96
|
+
'> Comentários frequentemente **corrigem ou substituem** instruções dos documentos ' +
|
|
97
|
+
'acima — em caso de conflito, o comentário mais recente prevalece.'
|
|
98
|
+
);
|
|
99
|
+
for (const group of comments) {
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(`### Comentários da ${group.kind} #${group.issueNumber}`);
|
|
102
|
+
if (group.total > group.items.length) {
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push(`_(mostrando os ${group.items.length} mais recentes de ${group.total})_`);
|
|
105
|
+
}
|
|
106
|
+
for (const c of group.items) {
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(`**${c.author || 'desconhecido'}** (${c.createdAt}):`);
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(c.body.trim());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Estado atual do código + spec.md + plan.md — fecho comum dos dois contextos.
|
|
116
|
+
function pushDigestSpecPlan(lines, { codeDigest, spec, plan, specPath, planPath }) {
|
|
117
|
+
if (codeDigest) {
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('## Estado atual do código');
|
|
120
|
+
lines.push('');
|
|
121
|
+
lines.push('> **NÃO reimplemente o que já existe; estenda os módulos listados abaixo.**');
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push(codeDigest.trim());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (spec) {
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`## spec.md (${specPath})`);
|
|
129
|
+
lines.push(spec.trim());
|
|
130
|
+
}
|
|
131
|
+
if (plan) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push(`## plan.md (${planPath})`);
|
|
134
|
+
lines.push(plan.trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
50
138
|
// Monta o markdown de contexto que será entregue ao spec-kit implement.
|
|
51
139
|
function buildContext({
|
|
52
140
|
type, issue, tasks, feature, siblingStories = [], spec, plan, specPath, planPath,
|
|
@@ -61,18 +149,7 @@ function buildContext({
|
|
|
61
149
|
lines.push(issue.body.trim());
|
|
62
150
|
}
|
|
63
151
|
|
|
64
|
-
|
|
65
|
-
if (blockedByWarnings.length > 0) {
|
|
66
|
-
lines.push('');
|
|
67
|
-
lines.push('## ⚠️ Dependências pendentes');
|
|
68
|
-
lines.push('');
|
|
69
|
-
for (const w of blockedByWarnings) lines.push(`- ${w}`);
|
|
70
|
-
lines.push('');
|
|
71
|
-
lines.push(
|
|
72
|
-
'**Implemente somente se tiver certeza de que a dependência não é bloqueante; ' +
|
|
73
|
-
'caso contrário, pare e reporte.**'
|
|
74
|
-
);
|
|
75
|
-
}
|
|
152
|
+
pushBlockedByWarnings(lines, blockedByWarnings);
|
|
76
153
|
|
|
77
154
|
// Modelo do board: "Etapa" (coluna do kanban) = DIREÇÃO, só avança; "Status"
|
|
78
155
|
// (Todo/In Progress/Done) = PROGRESSO dentro da etapa. O desenvolvimento de
|
|
@@ -81,11 +158,7 @@ function buildContext({
|
|
|
81
158
|
lines.push('');
|
|
82
159
|
lines.push('## Instruções de execução (uma task por vez, sequencial)');
|
|
83
160
|
lines.push('');
|
|
84
|
-
lines.push(
|
|
85
|
-
'Há **dois campos** no board com papéis diferentes — não os confunda:\n' +
|
|
86
|
-
`- **Etapa** (Backlog → … → ${STAGE_DEVELOPMENT} → ${STAGE_CODE_REVIEW} → … → ${STAGE_DONE}): a DIREÇÃO no kanban. Uma issue só **avança**, **nunca** volta para uma etapa anterior.\n` +
|
|
87
|
-
`- **Status** (${PROGRESS_TODO} → ${PROGRESS_IN_PROGRESS} → ${PROGRESS_DONE}): o **progresso dentro da etapa atual**. Ao avançar de etapa, o Status reinicia em ${PROGRESS_TODO} — exceto ao chegar na Etapa ${STAGE_DONE}, onde o Status fica **${PROGRESS_DONE}**.`
|
|
88
|
-
);
|
|
161
|
+
lines.push(boardFieldsExplainer());
|
|
89
162
|
lines.push('');
|
|
90
163
|
if (type === 'Story') {
|
|
91
164
|
lines.push(
|
|
@@ -125,10 +198,7 @@ function buildContext({
|
|
|
125
198
|
lines.push(`3. **Ao concluir:** **avance a Task #${issue.number} para a Etapa ${STAGE_DONE}** com Status **${PROGRESS_DONE}**.`);
|
|
126
199
|
}
|
|
127
200
|
lines.push('');
|
|
128
|
-
lines.push(
|
|
129
|
-
`> **Regra do board:** a **Etapa** só avança (nunca retrocede); o **Status** (${PROGRESS_TODO}/${PROGRESS_IN_PROGRESS}/${PROGRESS_DONE}) ` +
|
|
130
|
-
`mede o progresso dentro da etapa atual e reinicia a cada avanço (na Etapa ${STAGE_DONE}, o Status fica ${PROGRESS_DONE}).`
|
|
131
|
-
);
|
|
201
|
+
lines.push(boardRuleBlockquote());
|
|
132
202
|
|
|
133
203
|
lines.push('');
|
|
134
204
|
lines.push(`## Tasks a implementar — NESTA ORDEM (${tasks.length})`);
|
|
@@ -138,52 +208,136 @@ function buildContext({
|
|
|
138
208
|
if (t.body && t.body.trim()) lines.push(t.body.trim());
|
|
139
209
|
});
|
|
140
210
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
211
|
+
pushCommentsSection(lines, comments);
|
|
212
|
+
pushDigestSpecPlan(lines, { codeDigest, spec, plan, specPath, planPath });
|
|
213
|
+
|
|
214
|
+
lines.push('');
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Planeja a implementação de uma Feature: separa as Stories já implementadas
|
|
220
|
+
* (Etapa >= reviewStage na ordem canônica) das pendentes e ordena as pendentes
|
|
221
|
+
* topologicamente pelas dependências. Pura — sem I/O. NUNCA lança.
|
|
222
|
+
*
|
|
223
|
+
* Stories com stage null/desconhecido contam como pendentes (mais seguro
|
|
224
|
+
* incluir do que pular em silêncio). O grafo é montado só com as pendentes:
|
|
225
|
+
* dependências para Stories puladas (ou externas) contam como satisfeitas, e o
|
|
226
|
+
* `cycle` retornado só acusa ciclos entre pendentes.
|
|
227
|
+
*
|
|
228
|
+
* @param {Array<{number:number, stage:string|null, dependsOn?:number[]}>} stories
|
|
229
|
+
* @param {{reviewStage?:string, stageOrder?:string[]}} [opts]
|
|
230
|
+
* @returns {{ pending: object[], skipped: object[], cycle: number[] }}
|
|
231
|
+
* pending: objetos originais na ordem de execução; skipped: em ordem
|
|
232
|
+
* crescente de number; cycle: numbers pendentes em/bloqueados por ciclo.
|
|
233
|
+
*/
|
|
234
|
+
export function planFeatureImplementation(stories, {
|
|
235
|
+
reviewStage = STAGE_CODE_REVIEW,
|
|
236
|
+
stageOrder = STAGE_ORDER,
|
|
237
|
+
} = {}) {
|
|
238
|
+
const list = Array.isArray(stories) ? stories : [];
|
|
239
|
+
const reviewIdx = stageOrder.indexOf(reviewStage);
|
|
240
|
+
const skipped = [];
|
|
241
|
+
const pendingSet = [];
|
|
242
|
+
for (const s of list) {
|
|
243
|
+
const idx = s.stage ? stageOrder.indexOf(s.stage) : -1;
|
|
244
|
+
if (reviewIdx !== -1 && idx !== -1 && idx >= reviewIdx) skipped.push(s);
|
|
245
|
+
else pendingSet.push(s);
|
|
246
|
+
}
|
|
247
|
+
skipped.sort((a, b) => a.number - b.number);
|
|
248
|
+
|
|
249
|
+
const { order, cycle } = orderStories(
|
|
250
|
+
pendingSet.map(s => ({ number: s.number, dependsOn: s.dependsOn || [] }))
|
|
251
|
+
);
|
|
252
|
+
const byNumber = new Map(pendingSet.map(s => [s.number, s]));
|
|
253
|
+
const pending = order.map(n => byNumber.get(n)).filter(Boolean);
|
|
254
|
+
return { pending, skipped, cycle };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Monta o markdown de contexto do modo Feature (puro — testável): todas as
|
|
259
|
+
* Stories pendentes em ordem de execução, cada uma com suas Tasks, mais as já
|
|
260
|
+
* implementadas (não tocar), comentários, digest e spec/plan.
|
|
261
|
+
*/
|
|
262
|
+
export function buildFeatureContext({
|
|
263
|
+
feature, stories, skipped = [],
|
|
264
|
+
spec, plan, specPath, planPath,
|
|
265
|
+
comments = [], codeDigest = null, blockedByWarnings = [],
|
|
266
|
+
}) {
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push(`# Contexto de implementação — Feature #${feature.number}`);
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push(`**Feature:** ${feature.title}`);
|
|
271
|
+
if (feature.body && feature.body.trim()) {
|
|
146
272
|
lines.push('');
|
|
147
|
-
lines.push(
|
|
148
|
-
'> Comentários frequentemente **corrigem ou substituem** instruções dos documentos ' +
|
|
149
|
-
'acima — em caso de conflito, o comentário mais recente prevalece.'
|
|
150
|
-
);
|
|
151
|
-
for (const group of comments) {
|
|
152
|
-
lines.push('');
|
|
153
|
-
lines.push(`### Comentários da ${group.kind} #${group.issueNumber}`);
|
|
154
|
-
if (group.total > group.items.length) {
|
|
155
|
-
lines.push('');
|
|
156
|
-
lines.push(`_(mostrando os ${group.items.length} mais recentes de ${group.total})_`);
|
|
157
|
-
}
|
|
158
|
-
for (const c of group.items) {
|
|
159
|
-
lines.push('');
|
|
160
|
-
lines.push(`**${c.author || 'desconhecido'}** (${c.createdAt}):`);
|
|
161
|
-
lines.push('');
|
|
162
|
-
lines.push(c.body.trim());
|
|
163
|
-
}
|
|
164
|
-
}
|
|
273
|
+
lines.push(feature.body.trim());
|
|
165
274
|
}
|
|
166
275
|
|
|
167
|
-
|
|
168
|
-
|
|
276
|
+
pushBlockedByWarnings(lines, blockedByWarnings);
|
|
277
|
+
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push('## Instruções de execução (uma Story por vez, na ordem)');
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(boardFieldsExplainer());
|
|
282
|
+
lines.push('');
|
|
283
|
+
lines.push(
|
|
284
|
+
`Implemente as ${stories.length} story(ies) pendentes abaixo **uma de cada vez, na ordem listada** — ` +
|
|
285
|
+
'a ordem já respeita as dependências entre elas (linhas `Depende de:`). Para **cada Story**, na ordem:'
|
|
286
|
+
);
|
|
287
|
+
lines.push('');
|
|
288
|
+
lines.push(`1. **Ao começar a Story:** garanta que ela está na Etapa **${STAGE_DEVELOPMENT}** com Status **${PROGRESS_IN_PROGRESS}** (as Tasks dela nessa Etapa com Status **${PROGRESS_TODO}**).`);
|
|
289
|
+
lines.push(`2. Implemente as Tasks da Story **uma de cada vez, na ordem listada**. É PROIBIDO ter mais de uma Task com Status **${PROGRESS_IN_PROGRESS}** ao mesmo tempo. Para **cada Task**:`);
|
|
290
|
+
lines.push(` 1. **Ao começar:** Status da Task → **${PROGRESS_IN_PROGRESS}** (a Etapa continua ${STAGE_DEVELOPMENT}).`);
|
|
291
|
+
lines.push(' 2. **Implemente** a Task por completo.');
|
|
292
|
+
lines.push(` 3. **Ao concluir:** **avance a Task para a Etapa ${STAGE_DONE}** com Status **${PROGRESS_DONE}**.`);
|
|
293
|
+
lines.push(`3. **Ao concluir TODAS as Tasks da Story:** faça o **commit**, abra o **Pull Request** da Story e **avance a Etapa da Story para ${STAGE_CODE_REVIEW}** (Status ${PROGRESS_TODO}).`);
|
|
294
|
+
lines.push('4. Só então inicie a próxima Story.');
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push(
|
|
297
|
+
`**Feature #${feature.number}:** avance-a para a Etapa **${STAGE_CODE_REVIEW}** (Status ${PROGRESS_TODO}) ` +
|
|
298
|
+
`**somente após concluir a ÚLTIMA Story da lista**${skipped.length > 0 ? ' (as Stories já implementadas listadas abaixo não precisam ser refeitas)' : ''}. ` +
|
|
299
|
+
`Enquanto houver Story pendente, a Feature permanece em ${STAGE_DEVELOPMENT}.`
|
|
300
|
+
);
|
|
301
|
+
lines.push('');
|
|
302
|
+
lines.push(boardRuleBlockquote());
|
|
303
|
+
|
|
304
|
+
if (skipped.length > 0) {
|
|
169
305
|
lines.push('');
|
|
170
|
-
lines.push(
|
|
306
|
+
lines.push(`## Stories já implementadas — NÃO tocar (${skipped.length})`);
|
|
171
307
|
lines.push('');
|
|
172
|
-
lines.push(
|
|
308
|
+
lines.push(`> Já estão em ${STAGE_CODE_REVIEW} ou além; **não** as reimplemente nem as mova.`);
|
|
173
309
|
lines.push('');
|
|
174
|
-
|
|
310
|
+
for (const s of skipped) {
|
|
311
|
+
lines.push(`- #${s.number} ${s.title} — Etapa atual: ${s.stage || '—'}`);
|
|
312
|
+
}
|
|
175
313
|
}
|
|
176
314
|
|
|
177
|
-
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push(`## Stories a implementar — NESTA ORDEM (${stories.length})`);
|
|
317
|
+
stories.forEach((s, i) => {
|
|
178
318
|
lines.push('');
|
|
179
|
-
lines.push(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
319
|
+
lines.push(`### ${i + 1}. Story #${s.number} — ${s.title}`);
|
|
320
|
+
const depLine = formatDependencyLine(s.dependsOn || []);
|
|
321
|
+
if (depLine) {
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push(depLine);
|
|
324
|
+
}
|
|
325
|
+
if (s.body && s.body.trim()) {
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push(s.body.trim());
|
|
328
|
+
}
|
|
329
|
+
const tasks = s.tasks || [];
|
|
183
330
|
lines.push('');
|
|
184
|
-
lines.push(
|
|
185
|
-
|
|
186
|
-
|
|
331
|
+
lines.push(`#### Tasks da Story #${s.number} — NESTA ORDEM (${tasks.length})`);
|
|
332
|
+
tasks.forEach((t, j) => {
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(`##### ${j + 1}. #${t.number} ${t.title}`);
|
|
335
|
+
if (t.body && t.body.trim()) lines.push(t.body.trim());
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
pushCommentsSection(lines, comments);
|
|
340
|
+
pushDigestSpecPlan(lines, { codeDigest, spec, plan, specPath, planPath });
|
|
187
341
|
|
|
188
342
|
lines.push('');
|
|
189
343
|
return lines.join('\n');
|
|
@@ -194,6 +348,182 @@ function renderCommand(template, vars) {
|
|
|
194
348
|
return template.replace(/\{(\w+)\}/g, (m, key) => (key in vars ? vars[key] : m));
|
|
195
349
|
}
|
|
196
350
|
|
|
351
|
+
// Modo Feature: avalia as Stories da Feature (dependências + Etapa no board),
|
|
352
|
+
// pula as já implementadas (Code Review+) e monta UM contexto único com todas
|
|
353
|
+
// as pendentes em ordem topológica — spec-kit acionado uma vez.
|
|
354
|
+
async function implementFeature({ token, owner, repo, config, feature, featureDirOpt, dryRun }) {
|
|
355
|
+
// F1. Stories (sub-issues) da Feature.
|
|
356
|
+
const subs = await listSubIssues(token, feature.node_id).catch(() => []);
|
|
357
|
+
const stories = subs.filter(s => detectIssueType({ title: s.title, labels: s.labels }) === 'Story');
|
|
358
|
+
if (stories.length === 0) {
|
|
359
|
+
p.log.error(
|
|
360
|
+
`Feature #${feature.number} não tem Stories (sub-issues). ` +
|
|
361
|
+
'Decomponha primeiro: adicione a label spec-wave:decompose.'
|
|
362
|
+
);
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
p.log.info(`Feature com ${stories.length} story(ies): ${stories.map(s => `#${s.number}`).join(', ')}`);
|
|
367
|
+
|
|
368
|
+
// F2. Dependências de cada Story: linha "Depende de:" do body ∪ blocked_by nativo.
|
|
369
|
+
const enriched = await Promise.all(stories.map(async (s) => {
|
|
370
|
+
let body = s.body;
|
|
371
|
+
if (!body) body = (await getIssue(token, owner, repo, s.number).catch(() => null))?.body || '';
|
|
372
|
+
const deps = new Set(parseDependencies(body));
|
|
373
|
+
const blocked = await listBlockedBy(token, owner, repo, s.number).catch(() => []);
|
|
374
|
+
for (const b of blocked) deps.add(b.number);
|
|
375
|
+
return { number: s.number, title: s.title, nodeId: s.nodeId, body: body || '', dependsOn: [...deps] };
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
// F3. Etapa de cada Story no board (best-effort — sem board, nada é pulado).
|
|
379
|
+
const { project, error: projectError } = loadProjectConfig();
|
|
380
|
+
const stageOf = new Map();
|
|
381
|
+
if (projectError) {
|
|
382
|
+
p.log.warn(`${projectError} — Etapas do board não consultadas; nenhuma Story será considerada implementada.`);
|
|
383
|
+
} else {
|
|
384
|
+
const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
|
|
385
|
+
if (etapaField?.id) {
|
|
386
|
+
await Promise.all(enriched.map(async (s) => {
|
|
387
|
+
try {
|
|
388
|
+
const itemId = await addProjectItem(token, project.id, s.nodeId);
|
|
389
|
+
stageOf.set(s.number, await getItemSingleSelectValue(token, itemId, etapaField.id));
|
|
390
|
+
} catch {
|
|
391
|
+
stageOf.set(s.number, null);
|
|
392
|
+
}
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// F4. Planejamento: pendentes em ordem topológica, puladas, ciclos.
|
|
398
|
+
const { pending, skipped, cycle } = planFeatureImplementation(
|
|
399
|
+
enriched.map(s => ({ ...s, stage: stageOf.get(s.number) ?? null }))
|
|
400
|
+
);
|
|
401
|
+
if (cycle.length > 0) {
|
|
402
|
+
p.log.error(
|
|
403
|
+
`Ciclo de dependências entre Stories pendentes: ${cycle.map(n => `#${n}`).join(', ')}. ` +
|
|
404
|
+
'Corrija as linhas "Depende de:" (ou as relações blocked by) dessas Stories — ' +
|
|
405
|
+
`use \`spec-wave order ${feature.number}\` para visualizar.`
|
|
406
|
+
);
|
|
407
|
+
process.exitCode = 1;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (skipped.length > 0) {
|
|
411
|
+
p.log.info(
|
|
412
|
+
`Puladas (já em ${STAGE_CODE_REVIEW}+): ` +
|
|
413
|
+
skipped.map(s => `#${s.number} (${s.stage})`).join(', ')
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (pending.length === 0) {
|
|
417
|
+
p.outro(`Todas as ${stories.length} story(ies) da Feature #${feature.number} já estão implementadas — nada a fazer.`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
p.log.info(`Ordem de implementação: ${pending.map(s => `#${s.number}`).join(' → ')}`);
|
|
421
|
+
|
|
422
|
+
// F5. Tasks de cada Story pendente.
|
|
423
|
+
const noTasks = [];
|
|
424
|
+
for (const s of pending) {
|
|
425
|
+
const storySubs = await listSubIssues(token, s.nodeId).catch(() => []);
|
|
426
|
+
s.tasks = storySubs
|
|
427
|
+
.filter(t => detectIssueType({ title: t.title, labels: t.labels }) === 'Task')
|
|
428
|
+
.map(t => ({ number: t.number, title: t.title, body: t.body || '' }));
|
|
429
|
+
if (s.tasks.length === 0) noTasks.push(s.number);
|
|
430
|
+
}
|
|
431
|
+
if (noTasks.length > 0) {
|
|
432
|
+
p.log.error(
|
|
433
|
+
`Story(ies) pendente(s) sem Tasks (sub-issues): ${noTasks.map(n => `#${n}`).join(', ')}. ` +
|
|
434
|
+
'Decomponha-as antes de implementar (re-rode o decompose da Feature se necessário).'
|
|
435
|
+
);
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// F6. spec.md/plan.md — a issue-alvo JÁ é a Feature (sem resolveFeature).
|
|
441
|
+
const featureDir = featureDirOpt || path.join('docs', 'features', slugify(feature.title));
|
|
442
|
+
let specPlan = { spec: null, plan: null, specPath: null, planPath: null };
|
|
443
|
+
if (existsSync(featureDir)) {
|
|
444
|
+
specPlan = readSpecPlan(featureDir);
|
|
445
|
+
} else {
|
|
446
|
+
p.log.warn(`Diretório da feature não encontrado (${featureDir}); seguindo só com as Stories.`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// F7. Dependências EXTERNAS ainda abertas (da Feature e das Stories pendentes)
|
|
450
|
+
// — as internas ao conjunto de Stories já estão cobertas pela ordem topológica.
|
|
451
|
+
const blockedByWarnings = [];
|
|
452
|
+
try {
|
|
453
|
+
const internal = new Set(stories.map(s => s.number));
|
|
454
|
+
const featureDeps = new Set(parseDependencies(feature.body));
|
|
455
|
+
const featureBlocked = await listBlockedBy(token, owner, repo, feature.number).catch(() => []);
|
|
456
|
+
for (const b of featureBlocked) featureDeps.add(b.number);
|
|
457
|
+
const check = [
|
|
458
|
+
{ kind: 'Feature', number: feature.number, deps: featureDeps },
|
|
459
|
+
...pending.map(s => ({ kind: 'Story', number: s.number, deps: new Set(s.dependsOn) })),
|
|
460
|
+
];
|
|
461
|
+
for (const c of check) {
|
|
462
|
+
for (const depNumber of [...c.deps].sort((a, b) => a - b)) {
|
|
463
|
+
if (internal.has(depNumber)) continue;
|
|
464
|
+
const dep = await getIssue(token, owner, repo, depNumber).catch(() => null);
|
|
465
|
+
if (!dep || dep.state === 'closed') continue;
|
|
466
|
+
const warning =
|
|
467
|
+
`${c.kind} #${c.number} depende de #${depNumber} («${dep.title}»), ` +
|
|
468
|
+
`que ainda não está concluída (state: ${dep.state}).`;
|
|
469
|
+
blockedByWarnings.push(warning);
|
|
470
|
+
p.log.warn(warning);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch { /* aviso é best-effort — segue sem ele */ }
|
|
474
|
+
|
|
475
|
+
// F8. Comentários: Feature primeiro, depois cada Story pendente na ordem.
|
|
476
|
+
const comments = [];
|
|
477
|
+
const commentSources = [
|
|
478
|
+
{ number: feature.number, kind: 'Feature' },
|
|
479
|
+
...pending.map(s => ({ number: s.number, kind: 'Story' })),
|
|
480
|
+
];
|
|
481
|
+
for (const src of commentSources) {
|
|
482
|
+
const all = await listIssueComments(token, owner, repo, src.number).catch(() => []);
|
|
483
|
+
if (all.length === 0) continue;
|
|
484
|
+
const items = all.slice(-MAX_COMMENTS_PER_ISSUE).map(c => ({
|
|
485
|
+
...c,
|
|
486
|
+
body: c.body.length > MAX_COMMENT_CHARS
|
|
487
|
+
? `${c.body.slice(0, MAX_COMMENT_CHARS)}…[truncado]`
|
|
488
|
+
: c.body,
|
|
489
|
+
}));
|
|
490
|
+
comments.push({ issueNumber: src.number, kind: src.kind, total: all.length, items });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// F9. Digest do estado do código desde a criação da Feature.
|
|
494
|
+
let codeDigest = null;
|
|
495
|
+
try {
|
|
496
|
+
const paths = specPlan.plan ? extractPathsFromPlan(specPlan.plan) : [];
|
|
497
|
+
codeDigest = await buildCodeDigest({ sinceIso: feature.created_at || null, paths });
|
|
498
|
+
} catch {
|
|
499
|
+
codeDigest = null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// F10. Contexto único + spec-kit (uma execução).
|
|
503
|
+
const context = buildFeatureContext({
|
|
504
|
+
feature: { number: feature.number, title: feature.title, body: feature.body || '' },
|
|
505
|
+
stories: pending,
|
|
506
|
+
skipped,
|
|
507
|
+
...specPlan,
|
|
508
|
+
comments,
|
|
509
|
+
codeDigest,
|
|
510
|
+
blockedByWarnings,
|
|
511
|
+
});
|
|
512
|
+
writeContextAndRunSpecKit({
|
|
513
|
+
config,
|
|
514
|
+
issueNumber: feature.number,
|
|
515
|
+
type: 'Feature',
|
|
516
|
+
title: feature.title,
|
|
517
|
+
specPlan,
|
|
518
|
+
context,
|
|
519
|
+
dryRun,
|
|
520
|
+
outroSuccess:
|
|
521
|
+
`${chalk.green('✓')} Implementação acionada para a Feature #${feature.number} ` +
|
|
522
|
+
`(${pending.length} story(ies) pendente(s)).\n` +
|
|
523
|
+
` Próximo: acompanhe os PRs de cada Story; a Feature avança para ${STAGE_CODE_REVIEW} após a última.`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
197
527
|
export async function implement({ issue: issueArg, featureDir: featureDirOpt, dryRun }) {
|
|
198
528
|
const issueNumber = parseInt(String(issueArg).replace('#', ''), 10);
|
|
199
529
|
if (!Number.isInteger(issueNumber)) {
|
|
@@ -260,9 +590,13 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
260
590
|
} else if (type === 'Task') {
|
|
261
591
|
tasks = [{ number: issue.number, title: issue.title, body: issue.body || '' }];
|
|
262
592
|
p.log.info(`Task única #${issueNumber}.`);
|
|
593
|
+
} else if (type === 'Feature') {
|
|
594
|
+
// Modo Feature: Stories pendentes em ordem de dependência, contexto único.
|
|
595
|
+
await implementFeature({ token, owner, repo, config, feature: issue, featureDirOpt, dryRun });
|
|
596
|
+
return;
|
|
263
597
|
} else {
|
|
264
598
|
p.log.error(
|
|
265
|
-
`implement só aceita Story ou Task. Issue #${issueNumber} é do tipo ${type || 'desconhecido'}.`
|
|
599
|
+
`implement só aceita Feature, Story ou Task. Issue #${issueNumber} é do tipo ${type || 'desconhecido'}.`
|
|
266
600
|
);
|
|
267
601
|
process.exitCode = 1;
|
|
268
602
|
return;
|
|
@@ -298,8 +632,6 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
298
632
|
// 4c. Comentários das issues (best-effort) — revisões e correções vivem nos
|
|
299
633
|
// comentários, não no body; sem eles o agente implementa instruções já
|
|
300
634
|
// corrigidas. Feature primeiro (correções de escopo), depois a issue-alvo.
|
|
301
|
-
const MAX_COMMENTS_PER_ISSUE = 15;
|
|
302
|
-
const MAX_COMMENT_CHARS = 2000;
|
|
303
635
|
const comments = [];
|
|
304
636
|
const commentSources = [];
|
|
305
637
|
if (feature && feature.number !== issue.number) {
|
|
@@ -353,17 +685,27 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
353
685
|
}
|
|
354
686
|
} catch { /* aviso é best-effort — segue sem ele */ }
|
|
355
687
|
|
|
356
|
-
// 5. Monta e
|
|
688
|
+
// 5-6. Monta o contexto e aciona o spec-kit (comando configurável).
|
|
357
689
|
const context = buildContext({
|
|
358
690
|
type, issue, tasks, feature, siblingStories, ...specPlan,
|
|
359
691
|
comments, codeDigest, blockedByWarnings,
|
|
360
692
|
});
|
|
693
|
+
writeContextAndRunSpecKit({
|
|
694
|
+
config, issueNumber, type, title: issue.title, specPlan, context, dryRun,
|
|
695
|
+
outroSuccess:
|
|
696
|
+
`${chalk.green('✓')} Implementação acionada para ${type} #${issueNumber}.\n` +
|
|
697
|
+
' Próximo: revise as mudanças, abra o PR e mova o card para 👀 Code Review.',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Grava o arquivo de contexto e aciona o spec-kit (comando configurável) —
|
|
702
|
+
// fecho comum dos modos Feature e Story/Task.
|
|
703
|
+
function writeContextAndRunSpecKit({ config, issueNumber, type, title, specPlan, context, dryRun, outroSuccess }) {
|
|
361
704
|
mkdirSync(WORK_DIR, { recursive: true });
|
|
362
705
|
const tasksFile = path.join(WORK_DIR, `implement-${issueNumber}.md`);
|
|
363
706
|
writeFileSync(tasksFile, context);
|
|
364
707
|
p.log.success(`Contexto montado em ${chalk.cyan(tasksFile)}.`);
|
|
365
708
|
|
|
366
|
-
// 6. Aciona o spec-kit (comando configurável).
|
|
367
709
|
const template = process.env.SPEC_WAVE_IMPLEMENT_CMD || config.specKit?.command;
|
|
368
710
|
const vars = {
|
|
369
711
|
tasksFile,
|
|
@@ -371,7 +713,7 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
371
713
|
planFile: specPlan.planPath || '',
|
|
372
714
|
issue: String(issueNumber),
|
|
373
715
|
type,
|
|
374
|
-
title
|
|
716
|
+
title,
|
|
375
717
|
};
|
|
376
718
|
|
|
377
719
|
if (!template) {
|
|
@@ -404,8 +746,5 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
404
746
|
return;
|
|
405
747
|
}
|
|
406
748
|
|
|
407
|
-
p.outro(
|
|
408
|
-
`${chalk.green('✓')} Implementação acionada para ${type} #${issueNumber}.\n` +
|
|
409
|
-
' Próximo: revise as mudanças, abra o PR e mova o card para 👀 Code Review.'
|
|
410
|
-
);
|
|
749
|
+
p.outro(outroSuccess);
|
|
411
750
|
}
|
package/src/commands/info.mjs
CHANGED
|
@@ -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
|
|
package/src/commands/update.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
@@ -86,7 +86,7 @@ Labels de **estado** (gravadas pelas automações — **não** são gatilhos, n
|
|
|
86
86
|
- `spec-wave:critique-failed` → a crítica adversarial apontou contradições **graves** nos documentos; **bloqueia** o `spec-wave:ready` até ser removida (veja *Crítica adversarial* abaixo)
|
|
87
87
|
- `spec-wave:decomposed` → a Feature/RFC já foi decomposta; o `decompose` pula silenciosamente enquanto ela existir (veja *Guard de idempotência* abaixo)
|
|
88
88
|
|
|
89
|
-
A etapa **🚧 Desenvolvimento** é coberta pelo comando **local** `npx @spec-wave/cli implement <número>` (não é uma label/Action): lê uma Story ou Task e aciona o spec-kit para implementar. Veja `/spec-wave implement`.
|
|
89
|
+
A etapa **🚧 Desenvolvimento** é coberta pelo comando **local** `npx @spec-wave/cli 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`.
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
@@ -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 |
|
|
@@ -167,17 +169,17 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
|
|
|
167
169
|
> - `generate-spec` / `generate-plan` → **apenas Features**. Para **Spike, RFC e Bug** a geração é **pulada** (o Action remove a label e comenta) — esses tipos não usam spec/plan.
|
|
168
170
|
> - `decompose` → **Feature** (gera Stories + Tasks) e **RFC** (gera **Tasks** diretamente, sem Stories). Para outros tipos, o Action recusa.
|
|
169
171
|
|
|
170
|
-
### `@spec-wave/cli implement` — aciona o spec-kit para uma Story ou Task (comando LOCAL)
|
|
172
|
+
### `@spec-wave/cli implement` — aciona o spec-kit para uma Feature, Story ou Task (comando LOCAL)
|
|
171
173
|
| Flag/Arg | Tipo | Descrição |
|
|
172
174
|
|----------|------|-----------|
|
|
173
|
-
| `<issue>` | string (obrigatório) | Número da issue (Story ou Task), ex.: `12` ou `#12`. Argumento posicional. |
|
|
175
|
+
| `<issue>` | string (obrigatório) | Número da issue (Feature, Story ou Task), ex.: `12` ou `#12`. Argumento posicional. |
|
|
174
176
|
| `--feature-dir <path>` | string | Caminho `docs/features/<slug>` para anexar `spec.md`/`plan.md` como contexto (sobrescreve a resolução automática). |
|
|
175
177
|
| `--dry-run` | flag | Monta o contexto e imprime o comando do spec-kit **sem executar**. |
|
|
176
178
|
|
|
177
|
-
> 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.
|
|
179
|
+
> Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Feature** → lista as Stories (sub-issues), **ordena topologicamente pelas dependências** (`Depende de:` + *blocked by*), **pula** as já em 👀 Code Review+ (listadas no contexto como "não tocar") e monta **um único** contexto com todas as pendentes (cada uma com suas Tasks), acionando o spec-kit **uma vez** com `{issue}/{type}/{title}` da Feature — **ciclo de dependências entre Stories pendentes aborta o comando (exit 1)**; **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
|
|
|
@@ -226,7 +228,8 @@ O evento `labeled` pode redisparar (re-add da label, retry de runner). Para não
|
|
|
226
228
|
|
|
227
229
|
O `decompose` grava nas Stories geradas uma linha **`Depende de: #N, #M`** no corpo e cria a relação nativa *blocked by* do GitHub. Essas dependências alimentam:
|
|
228
230
|
- `spec-wave order <feature>` → ordem topológica de execução;
|
|
229
|
-
- `spec-wave implement <
|
|
231
|
+
- `spec-wave implement <feature>` → Stories pendentes implementadas **nessa ordem**; **ciclo de dependências → erro** (corrija as linhas `Depende de:`); dependências **externas** abertas viram aviso no contexto;
|
|
232
|
+
- `spec-wave implement <n>` (Story/Task) → **aviso** no contexto quando uma dependência ainda não está concluída (confirme com o usuário antes de implementar fora de ordem).
|
|
230
233
|
|
|
231
234
|
Não apague a linha `Depende de:` ao editar o corpo de uma Story; para mudar dependências, edite a linha (e/ou a relação *blocked by*).
|
|
232
235
|
|
|
@@ -263,6 +266,7 @@ Mostra se o repositório atual já foi configurado com o spec-wave.
|
|
|
263
266
|
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
267
|
- Se sim → siga o fluxo de `/spec-wave setup`.
|
|
265
268
|
- Se não → encerre sem alterar nada.
|
|
269
|
+
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
270
|
|
|
267
271
|
---
|
|
268
272
|
|
|
@@ -470,20 +474,22 @@ Para qualquer outro tipo (Spike, Bug, Story, Task, …) o Action **recusa** e co
|
|
|
470
474
|
gh issue edit <número> --add-label "spec-wave:decompose"
|
|
471
475
|
```
|
|
472
476
|
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.
|
|
477
|
+
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
478
|
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
479
|
|
|
476
480
|
---
|
|
477
481
|
|
|
478
482
|
### `/spec-wave implement <número-da-issue>`
|
|
479
483
|
|
|
480
|
-
Aciona o spec-kit para implementar uma **Story** (todas as suas Tasks) ou uma **Task** isolada. Comando **local** (etapa 🚧 Desenvolvimento) — não usa label/Action.
|
|
484
|
+
Aciona o spec-kit para implementar uma **Feature** (todas as Stories pendentes, em ordem de dependência), uma **Story** (todas as suas Tasks) ou uma **Task** isolada. Comando **local** (etapa 🚧 Desenvolvimento) — não usa label/Action.
|
|
485
|
+
|
|
486
|
+
**Pré-requisitos:** o repositório atual precisa estar inicializado (`.spec-wave.json` presente) e a issue deve ser do tipo Feature, Story ou Task. Para executar de fato (fora do `--dry-run`), o spec-kit precisa estar configurado via `specKit.command` no `.spec-wave.json` ou a env `SPEC_WAVE_IMPLEMENT_CMD`.
|
|
481
487
|
|
|
482
|
-
**
|
|
488
|
+
**Modo Feature:** o comando avalia as Stories da Feature — ordena topologicamente pelas dependências (`Depende de:` + *blocked by*), consulta a Etapa de cada uma no board e **pula as já implementadas** (👀 Code Review ou além). O contexto único (`.spec-wave/implement-<feature>.md`) traz as pendentes em ordem, cada uma com suas Tasks. **Ciclo de dependências entre Stories pendentes → o comando aborta** (corrija as linhas `Depende de:`; use `npx @spec-wave/cli order <feature>` para visualizar). Story pendente sem Tasks → aborta pedindo decomposição. Todas implementadas → encerra sem acionar o spec-kit.
|
|
483
489
|
|
|
484
490
|
**Passos:**
|
|
485
491
|
1. Confirme que há `.spec-wave.json` no repo (senão, oriente `/spec-wave setup`).
|
|
486
|
-
2. **Sempre comece com `--dry-run`** para inspecionar o que será feito — detecção do tipo, lista de Tasks coletadas (
|
|
492
|
+
2. **Sempre comece com `--dry-run`** para inspecionar o que será feito — detecção do tipo, lista de Tasks coletadas (Story) ou a ordem/puladas/ciclos das Stories (Feature) e o comando do spec-kit que seria executado:
|
|
487
493
|
```bash
|
|
488
494
|
npx @spec-wave/cli implement <número> --dry-run
|
|
489
495
|
```
|
|
@@ -495,8 +501,8 @@ Aciona o spec-kit para implementar uma **Story** (todas as suas Tasks) ou uma **
|
|
|
495
501
|
```
|
|
496
502
|
- Se o spec-kit **não** estiver configurado, o comando só monta o contexto e mostra como configurar (`specKit.command` / `SPEC_WAVE_IMPLEMENT_CMD`). Ajude o usuário a definir o template (placeholders: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`).
|
|
497
503
|
- Use `--feature-dir docs/features/<slug>` se a resolução automática da Feature falhar (a skill avisa com warning) e você quiser anexar `spec.md`/`plan.md` como contexto.
|
|
498
|
-
6. Se a issue **não** for Story nem Task (ex.:
|
|
499
|
-
7. Ao final (Tasks em **🎉 Done**, Story em **👀 Code Review**; a Feature só vai para Code Review quando a última Story concluir): confirme o resultado com o usuário e oriente a revisão
|
|
504
|
+
6. **No modo Feature**, siga o contexto Story a Story, na ordem listada: para cada Story pendente, implemente as Tasks com `task start`/`task done`, depois commit + PR + `npx @spec-wave/cli story review <n>`; só então passe à próxima Story. Se a issue **não** for Feature, Story nem Task (ex.: Bug, Spike, Epic), o comando recusa. Feature **sem Stories** → rode `/spec-wave decompose` primeiro. **Ciclo de dependências** → corrija as linhas `Depende de:` (veja `spec-wave order`).
|
|
505
|
+
7. Ao final (Tasks em **🎉 Done**, Story em **👀 Code Review**; a Feature só vai para Code Review quando a última Story concluir — no modo Feature, isso acontece dentro da mesma execução): confirme o resultado com o usuário e oriente a revisão dos PRs.
|
|
500
506
|
|
|
501
507
|
---
|
|
502
508
|
|