@spec-wave/cli 0.15.0 → 0.16.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.
Files changed (78) hide show
  1. package/README.md +1 -0
  2. package/bin/spec-wave.mjs +44 -5
  3. package/package.json +8 -2
  4. package/src/agent/anthropic-agent.mjs +337 -0
  5. package/src/agent/errors.mjs +33 -0
  6. package/src/agent/index.mjs +108 -0
  7. package/src/agent/openrouter-agent.mjs +378 -0
  8. package/src/agent/run-types.mjs +59 -0
  9. package/src/agent/telemetry.mjs +54 -0
  10. package/src/agent/tools.mjs +452 -0
  11. package/src/agent/tracing.mjs +106 -0
  12. package/src/api/github-graphql.mjs +23 -1
  13. package/src/api/github-rest.mjs +8 -0
  14. package/src/commands/bug.mjs +8 -0
  15. package/src/commands/code-review.mjs +45 -4
  16. package/src/commands/decompose.mjs +22 -72
  17. package/src/commands/dev-agent.mjs +3 -3
  18. package/src/commands/doctor.mjs +77 -6
  19. package/src/commands/generate-bug.mjs +195 -0
  20. package/src/commands/generate-plan.mjs +19 -44
  21. package/src/commands/generate-spec.mjs +18 -46
  22. package/src/commands/implement.mjs +105 -2
  23. package/src/commands/init.mjs +3 -3
  24. package/src/commands/install-skill.mjs +72 -16
  25. package/src/commands/issue.mjs +9 -7
  26. package/src/commands/move.mjs +11 -1
  27. package/src/commands/qa.mjs +23 -2
  28. package/src/commands/refresh.mjs +171 -5
  29. package/src/commands/triage.mjs +174 -0
  30. package/src/commands/update.mjs +16 -3
  31. package/src/commands/validate.mjs +82 -10
  32. package/src/config.mjs +159 -1
  33. package/src/lib/bug-context.mjs +160 -0
  34. package/src/lib/bug-doc.mjs +51 -0
  35. package/src/lib/bug-triage.mjs +81 -0
  36. package/src/lib/claude.mjs +71 -254
  37. package/src/lib/critique.mjs +43 -30
  38. package/src/lib/flow-run.mjs +145 -0
  39. package/src/lib/implement-board.mjs +12 -1
  40. package/src/lib/plugin-skills.mjs +122 -0
  41. package/src/lib/project-root.mjs +9 -2
  42. package/src/lib/prompt-loader.mjs +257 -0
  43. package/src/lib/skill-file.mjs +35 -0
  44. package/src/plugin/.claude-plugin/plugin.json +20 -0
  45. package/src/plugin/README.md +73 -0
  46. package/src/plugin/skills/bug/SKILL.md +60 -0
  47. package/src/plugin/skills/bug/model-prompt.critique.md +48 -0
  48. package/src/plugin/skills/bug/model-prompt.md +74 -0
  49. package/src/plugin/skills/decompose/SKILL.md +117 -0
  50. package/src/plugin/skills/decompose/model-prompt.critique.md +46 -0
  51. package/src/plugin/skills/decompose/model-prompt.feature.md +69 -0
  52. package/src/plugin/skills/decompose/model-prompt.rfc.md +52 -0
  53. package/src/plugin/skills/doctor/SKILL.md +51 -0
  54. package/src/plugin/skills/fix-pr/SKILL.md +130 -0
  55. package/src/plugin/skills/implement/SKILL.md +102 -0
  56. package/src/plugin/skills/info/SKILL.md +40 -0
  57. package/src/plugin/skills/issue/SKILL.md +63 -0
  58. package/src/plugin/skills/move/SKILL.md +52 -0
  59. package/src/plugin/skills/order/SKILL.md +36 -0
  60. package/src/plugin/skills/plan/SKILL.md +58 -0
  61. package/src/plugin/skills/plan/model-prompt.critique.md +44 -0
  62. package/src/plugin/skills/plan/model-prompt.md +59 -0
  63. package/src/plugin/skills/plan/reference/tech-context.md +56 -0
  64. package/src/plugin/skills/ready/SKILL.md +44 -0
  65. package/src/plugin/skills/rfc/SKILL.md +47 -0
  66. package/src/plugin/skills/setup/SKILL.md +67 -0
  67. package/src/plugin/skills/spec/SKILL.md +55 -0
  68. package/src/plugin/skills/spec/model-prompt.md +61 -0
  69. package/src/plugin/skills/story/SKILL.md +49 -0
  70. package/src/plugin/skills/task/SKILL.md +41 -0
  71. package/src/plugin/skills/triage/SKILL.md +52 -0
  72. package/src/plugin/skills/uninstall/SKILL.md +43 -0
  73. package/src/plugin/skills/update/SKILL.md +51 -0
  74. package/src/plugin/skills/workflow/SKILL.md +158 -0
  75. package/src/templates/skill/SKILL.md +54 -4
  76. package/src/templates/workflows/generate-bug.yml +36 -0
  77. package/src/templates/workflows/validate.yml +2 -1
  78. package/src/ui/wizard.mjs +5 -2
package/src/config.mjs CHANGED
@@ -35,7 +35,7 @@ export const DEFAULT_PROVIDER = 'anthropic';
35
35
  // Ações de IA que podem ter modelo próprio no .spec-wave.json (bloco
36
36
  // `ai.models`, ex.: { "critique": "claude-opus-4-1" }). Resolvidas em runtime
37
37
  // por resolveAiConfig() em src/lib/claude.mjs.
38
- export const AI_ACTIONS = ['spec', 'plan', 'decompose', 'critique'];
38
+ export const AI_ACTIONS = ['spec', 'plan', 'decompose', 'critique', 'bug'];
39
39
 
40
40
  // Override de modelo POR EXECUÇÃO: a label `spec-wave:model:<apelido>` na issue
41
41
  // aponta para uma entrada de `ai.modelAliases` do .spec-wave.json. Serve para
@@ -59,6 +59,12 @@ export function getProvider(value) {
59
59
 
60
60
  export const STATUS_OPTIONS = [
61
61
  { name: '📥 Backlog', color: 'GRAY' },
62
+ // 🐞 Triagem é a porta de entrada do Bug reportado (RFC-004 §4). Entra AQUI, e
63
+ // não no fim da lista, porque shouldAdvanceStage compara índices RELATIVOS em
64
+ // STAGE_ORDER: inserir no meio preserva a validade de todo par (atual, destino)
65
+ // que já funcionava, enquanto pôr no fim tornaria "Triagem → qualquer coisa"
66
+ // um retrocesso e travaria o fluxo inteiro do Bug.
67
+ { name: '🐞 Triagem', color: 'RED' },
62
68
  { name: '🎯 Priorizado', color: 'BLUE' },
63
69
  { name: '📋 Spec', color: 'YELLOW' },
64
70
  { name: '📋 Plan', color: 'YELLOW' },
@@ -77,14 +83,74 @@ export const STATUS_OPTIONS = [
77
83
  // • "Status" (campo nativo: Todo/In Progress/Done): o PROGRESSO dentro da etapa
78
84
  // atual. Ao avançar de etapa, o Status reinicia em "Todo".
79
85
 
86
+ // Etapas que JÁ FORAM canônicas e saíram do fluxo. Um board antigo ainda as
87
+ // tem como opção do campo Etapa, e o `.spec-wave.json` gerado na época ainda
88
+ // guarda o id delas. São reportadas pelo doctor à parte das colunas criadas à
89
+ // mão, porque a orientação é outra: aqui a coluna não deve ser adaptada, deve
90
+ // ser esvaziada e removida.
91
+ export const RETIRED_STAGES = [
92
+ { name: '📋 Backlog Técnico', removedIn: '0.10.0', replacedBy: '✅ Ready' },
93
+ ];
94
+
80
95
  // Etapas (campo Etapa) referenciadas pelo fluxo de implementação.
96
+ export const STAGE_TRIAGE = STATUS_OPTIONS.find(s => s.name.includes('Triagem')).name;
81
97
  export const STAGE_READY = STATUS_OPTIONS.find(s => s.name.includes('Ready')).name;
82
98
  export const STAGE_DEVELOPMENT = STATUS_OPTIONS.find(s => s.name.includes('Desenvolvimento')).name;
83
99
  export const STAGE_CODE_REVIEW = STATUS_OPTIONS.find(s => s.name.includes('Code Review')).name;
100
+ export const STAGE_QA = STATUS_OPTIONS.find(s => s.name.includes('QA')).name;
101
+ export const STAGE_UAT = STATUS_OPTIONS.find(s => s.name.includes('Homologação')).name;
102
+ export const STAGE_DEPLOY = STATUS_OPTIONS.find(s => s.name.includes('Deploy')).name;
84
103
  export const STAGE_DONE = STATUS_OPTIONS.find(s => s.name.includes('Done')).name;
85
104
  // Ordem canônica das etapas — usada para garantir que uma issue só AVANÇA.
86
105
  export const STAGE_ORDER = STATUS_OPTIONS.map(s => s.name);
87
106
 
107
+ // Trilha de cada tipo de work item: as etapas que ele DE FATO percorre, na
108
+ // ordem (RFC-001 §4, RFC-004 §4). É documentação executável, não regra dura — a
109
+ // única regra dura do board continua sendo shouldAdvanceStage ("só avança").
110
+ // isStageInTrack serve para AVISAR quem move um item para fora da trilha dele
111
+ // (ex.: um Bug para 📋 Homologação), não para bloquear: bloquear exigiria que
112
+ // todo chamador de advanceToStage soubesse o tipo do item, e dois deles não
113
+ // sabem. Um tipo ausente daqui não tem trilha declarada e nunca gera aviso.
114
+ export const STAGE_TRACKS = {
115
+ Feature: STAGE_ORDER.filter(s => s !== STAGE_TRIAGE),
116
+ Story: [STAGE_READY, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_QA, STAGE_UAT, STAGE_DONE],
117
+ Task: [STAGE_READY, STAGE_DEVELOPMENT, STAGE_DONE],
118
+ Bug: [STAGE_TRIAGE, STAGE_READY, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_QA, STAGE_DEPLOY, STAGE_DONE],
119
+ RFC: [STATUS_OPTIONS[0].name, STAGE_READY, STAGE_DEVELOPMENT, STAGE_DONE],
120
+ };
121
+
122
+ /**
123
+ * A etapa em que um item NASCE (função PURA).
124
+ *
125
+ * Quase todo tipo nasce em 📥 Backlog. O Bug é a exceção: nasce em 🐞 Triagem,
126
+ * porque um defeito reportado precisa ser confirmado antes de virar fila. A
127
+ * exceção da exceção é o Bug P0 — a urgência inverte a ordem, ele nasce em
128
+ * ✅ Ready e a triagem é confirmada depois (RFC-004 §4.3).
129
+ *
130
+ * @param {string} type tipo do work item ('Feature', 'Bug', …)
131
+ * @param {string|null} [severity] prioridade, quando já conhecida ('P0'…'P3')
132
+ * @returns {string} nome da etapa inicial
133
+ */
134
+ export function initialStageForType(type, severity = null) {
135
+ if (type !== 'Bug') return STATUS_OPTIONS[0].name;
136
+ return severity === 'P0' ? STAGE_READY : STAGE_TRIAGE;
137
+ }
138
+
139
+ /**
140
+ * A etapa pertence à trilha declarada do tipo? (função PURA)
141
+ *
142
+ * Tipo sem trilha declarada devolve `true` — a ausência de trilha não é motivo
143
+ * para avisar nada.
144
+ *
145
+ * @param {string} type tipo do work item
146
+ * @param {string} stage nome da etapa
147
+ * @returns {boolean}
148
+ */
149
+ export function isStageInTrack(type, stage) {
150
+ const track = STAGE_TRACKS[type];
151
+ return track ? track.includes(stage) : true;
152
+ }
153
+
88
154
  // Valores do campo nativo "Status" (progresso dentro da etapa).
89
155
  export const PROGRESS_TODO = 'Todo';
90
156
  export const PROGRESS_IN_PROGRESS = 'In Progress';
@@ -199,6 +265,70 @@ export const PRIORITY_LABELS = [
199
265
  export const LABEL_DECOMPOSE = 'spec-wave:decompose';
200
266
  export const LABEL_DECOMPOSE_APPLY = 'spec-wave:decompose-apply';
201
267
 
268
+ // Label de gatilho da fila do dev-agent (o daemon `spec-wave-agent`): aplicá-la
269
+ // numa issue coloca a issue na fila que o agente consulta com `gh issue list`.
270
+ // Precisa estar registrada aqui porque `update` remove TODA label `spec-wave:*`
271
+ // que não esteja em ALL_LABELS — sem esta entrada, um `spec-wave update` de
272
+ // rotina apagava a label da fila e desligava o agente sem aviso.
273
+ export const LABEL_DEV_AGENT = 'spec-wave:dev-agent';
274
+
275
+ // Gatilho e estado do bug.md (RFC-004 §5). O bug.md é o artefato do Bug — leve
276
+ // por decisão: um defeito não gera spec.md + plan.md.
277
+ export const LABEL_BUG = 'spec-wave:bug';
278
+ export const LABEL_BUG_APPROVED = 'spec-wave:bug-approved';
279
+
280
+ // Desfecho da triagem do PM (RFC-004 §4.1). São mutuamente exclusivas: um bug
281
+ // triado foi aceito, rejeitado ou marcado como duplicata.
282
+ export const LABEL_TRIAGED = 'spec-wave:triaged';
283
+ export const LABEL_DUPLICATE = 'spec-wave:duplicate';
284
+ export const LABEL_WONT_FIX = 'spec-wave:wont-fix';
285
+
286
+ // Regressão pós-deploy (origem d): defeito aberto contra uma release entregue.
287
+ export const LABEL_REGRESSION = 'spec-wave:regression';
288
+
289
+ // Origem do defeito — as quatro portas de entrada do RFC-004 §4.2, detalhadas
290
+ // em seis rótulos. É o que torna possível medir DE ONDE vêm os bugs, e portanto
291
+ // qual portão do processo está deixando passar.
292
+ //
293
+ // Vive em label, não em campo do Projects v2, porque SnapshotItem.labels já
294
+ // chega ao client sem mudança nenhuma de contrato — um campo novo exigiria
295
+ // mexer em toSnapshotItem, no ProjectConfig e no refresh de todo repo.
296
+ export const BUG_ORIGINS = ['qa', 'uat', 'support', 'dev', 'review', 'regression'];
297
+ export const BUG_ORIGIN_PREFIX = 'spec-wave:origin:';
298
+
299
+ const BUG_ORIGIN_DESCRIPTIONS = {
300
+ qa: 'Bug encontrado na reprovação de QA',
301
+ uat: 'Bug encontrado na reprovação da Homologação',
302
+ support: 'Bug reportado por suporte ou usuário em produção',
303
+ dev: 'Bug encontrado pelo dev durante o desenvolvimento',
304
+ review: 'Bug encontrado no Code Review',
305
+ regression: 'Regressão detectada após o deploy',
306
+ };
307
+
308
+ /**
309
+ * Label de origem a partir do identificador (função PURA).
310
+ *
311
+ * @param {string} origin um de BUG_ORIGINS
312
+ * @returns {string|null} a label, ou null se a origem não existir
313
+ */
314
+ export function bugOriginLabel(origin) {
315
+ return BUG_ORIGINS.includes(origin) ? `${BUG_ORIGIN_PREFIX}${origin}` : null;
316
+ }
317
+
318
+ /**
319
+ * Origem a partir das labels de uma issue (função PURA).
320
+ *
321
+ * @param {object|Array} issueOrLabels issue ou array de labels
322
+ * @returns {string|null} a origem, ou null quando não há
323
+ */
324
+ export function bugOriginFromLabels(issueOrLabels) {
325
+ const found = labelNames(issueOrLabels)
326
+ .find(n => n.startsWith(BUG_ORIGIN_PREFIX));
327
+ if (!found) return null;
328
+ const origin = found.slice(BUG_ORIGIN_PREFIX.length);
329
+ return BUG_ORIGINS.includes(origin) ? origin : null;
330
+ }
331
+
202
332
  // Labels de estado gravadas pelas automações (não são gatilhos do usuário).
203
333
  export const LABEL_CRITIQUE_FAILED = 'spec-wave:critique-failed';
204
334
  export const LABEL_DECOMPOSED = 'spec-wave:decomposed';
@@ -212,6 +342,18 @@ export const TRIGGER_LABELS = [
212
342
  { name: 'spec-wave:plan-approved', color: '0E8A16', description: 'Spec+plan validados com sucesso' },
213
343
  { name: LABEL_DECOMPOSE, color: 'BFD4F2', description: 'Gerar/re-criticar o rascunho da decomposição (decomposition.md)' },
214
344
  { name: LABEL_DECOMPOSE_APPLY, color: 'BFD4F2', description: 'Aplicar o decomposition.md revisado: criar Stories e Tasks' },
345
+ { name: LABEL_DEV_AGENT, color: '5319E7', description: 'Enfileira a issue para o dev-agent autônomo' },
346
+ { name: LABEL_BUG, color: 'BFD4F2', description: 'Gerar bug.md via GitHub Action' },
347
+ { name: LABEL_BUG_APPROVED, color: '0E8A16', description: 'bug.md validado (reprodução, causa raiz e teste de regressão)' },
348
+ { name: LABEL_TRIAGED, color: '0E8A16', description: 'Bug triado pelo PM (severidade, origem e pai definidos)' },
349
+ { name: LABEL_DUPLICATE, color: 'EDEDED', description: 'Duplicata de outra issue (o corpo aponta qual)' },
350
+ { name: LABEL_WONT_FIX, color: 'EDEDED', description: 'Rejeitado na triagem — não será corrigido' },
351
+ { name: LABEL_REGRESSION, color: 'B60205', description: 'Regressão detectada após o deploy' },
352
+ ...BUG_ORIGINS.map(o => ({
353
+ name: `${BUG_ORIGIN_PREFIX}${o}`,
354
+ color: 'D93F0B',
355
+ description: BUG_ORIGIN_DESCRIPTIONS[o],
356
+ })),
215
357
  { name: LABEL_DECOMPOSE_READY, color: '0E8A16', description: 'Rascunho de decomposição pronto para revisão humana' },
216
358
  { name: LABEL_CRITIQUE_FAILED, color: 'B60205', description: 'Crítica adversarial apontou contradições graves' },
217
359
  { name: LABEL_NEEDS_HUMAN, color: 'B60205', description: 'Crítica reprovou N vezes seguidas — precisa de revisão humana' },
@@ -236,6 +378,7 @@ export function labelNames(issueOrLabels) {
236
378
  }
237
379
 
238
380
  export const WORKFLOW_FILES = [
381
+ 'generate-bug.yml',
239
382
  'generate-plan.yml',
240
383
  'generate-spec.yml',
241
384
  'validate.yml',
@@ -255,6 +398,21 @@ export const REQUIRED_SPEC_SECTIONS = [
255
398
  'Requisitos Não-Funcionais',
256
399
  ];
257
400
 
401
+ // Seções obrigatórias do bug.md (RFC-004 §5). Deliberadamente seis, e leves: o
402
+ // que o corretor precisa saber para reproduzir, achar a causa e provar o fix.
403
+ //
404
+ // ⚠️ validate compara com `content.includes('# ' + secao)` — byte a byte. Nada
405
+ // de caractere exótico aqui (ex.: '×' U+00D7), e o prompt do generate-bug lê
406
+ // ESTA constante para emitir exatamente estes títulos.
407
+ export const REQUIRED_BUG_SECTIONS = [
408
+ 'Reprodução',
409
+ 'Esperado e Obtido',
410
+ 'Impacto e Severidade',
411
+ 'Causa Raiz',
412
+ 'Escopo do Fix',
413
+ 'Teste de Regressão',
414
+ ];
415
+
258
416
  export const REQUIRED_PLAN_SECTIONS = [
259
417
  'Estratégia Técnica',
260
418
  'Detalhamento da Implementação',
@@ -0,0 +1,160 @@
1
+ // Contexto de implementação de um Bug (RFC-004 §7.1) — puro, testável.
2
+ //
3
+ // A diferença para o contexto de Story não é o formato, é a ORDEM DO TRABALHO.
4
+ // Uma Story tem tasks a executar; um Bug tem um defeito a entender antes de
5
+ // tocar em qualquer coisa. Por isso o contexto impõe quatro fases —
6
+ // reproduzir → causa raiz → fix mínimo → teste de regressão — e a primeira
7
+ // entrega é um teste que FALHA.
8
+ //
9
+ // A ordem existe porque a alternativa é o modo de falha clássico da correção
10
+ // assistida: o executor lê o sintoma, encontra o lugar onde ele se manifesta,
11
+ // remenda ali, e o defeito reaparece na próxima entrada.
12
+
13
+ import { STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, PROGRESS_IN_PROGRESS } from '../config.mjs';
14
+
15
+ /**
16
+ * Monta o markdown do contexto de um Bug (função PURA).
17
+ *
18
+ * @param {object} params
19
+ * @param {{number:number,title:string,body?:string}} params.bug
20
+ * @param {string|null} [params.bugDoc] conteúdo de docs/bugs/<slug>/bug.md
21
+ * @param {{number:number,title:string,kind:string}|null} [params.parent]
22
+ * @param {Array} [params.comments] grupos de comentários (mesmo shape do implement)
23
+ * @param {string|null} [params.codeDigest]
24
+ * @param {string[]} [params.blockedByWarnings]
25
+ * @param {string|null} [params.severity] P0–P3
26
+ * @returns {string}
27
+ */
28
+ export function buildBugContext({
29
+ bug, bugDoc = null, parent = null, comments = [], codeDigest = null,
30
+ blockedByWarnings = [], severity = null,
31
+ }) {
32
+ const lines = [];
33
+ lines.push(`# Contexto de correção — Bug #${bug.number}`);
34
+ lines.push('');
35
+ lines.push(`**Bug:** ${bug.title}`);
36
+ if (severity) lines.push(`**Severidade:** ${severity}`);
37
+ if (parent) {
38
+ lines.push(`**Item afetado:** ${parent.kind} #${parent.number} — ${parent.title}`);
39
+ }
40
+
41
+ if (bug.body && bug.body.trim()) {
42
+ lines.push('');
43
+ lines.push('## Relato original');
44
+ lines.push('');
45
+ lines.push(bug.body.trim());
46
+ }
47
+
48
+ if (blockedByWarnings.length > 0) {
49
+ lines.push('');
50
+ lines.push('## ⚠️ Dependências declaradas');
51
+ for (const w of blockedByWarnings) lines.push(`- ${w}`);
52
+ }
53
+
54
+ lines.push('');
55
+ lines.push('## Como corrigir (quatro fases, nesta ordem)');
56
+ lines.push('');
57
+ lines.push(
58
+ '> **Não comece pelo fix.** O modo de falha desta tarefa é encontrar o lugar onde o ' +
59
+ 'sintoma aparece, remendar ali, e o defeito voltar na próxima entrada. As fases 1 e 2 ' +
60
+ 'existem para impedir isso.'
61
+ );
62
+ lines.push('');
63
+ lines.push('### 1. Reproduzir');
64
+ lines.push('');
65
+ lines.push(
66
+ 'Escreva um teste que **falha** por causa deste defeito, seguindo os passos de reprodução. ' +
67
+ 'Rode-o e confirme que falha **pelo motivo certo** — um teste que falha por erro de ' +
68
+ 'digitação no próprio teste não reproduz nada. Se não conseguir reproduzir, **pare e ' +
69
+ 'reporte**: sem reprodução não há como provar que a correção funcionou.'
70
+ );
71
+ lines.push('');
72
+ lines.push('### 2. Causa raiz');
73
+ lines.push('');
74
+ lines.push(
75
+ 'Investigue até a **origem**, não até o lugar onde o erro se manifesta. Um valor nulo ' +
76
+ 'que estoura numa função raramente nasceu ali. Cite arquivo e função. Se o `bug.md` já ' +
77
+ 'traz uma causa raiz, **confirme-a contra o código** antes de aceitar — ela foi escrita ' +
78
+ 'por outro modelo, sem executar nada.'
79
+ );
80
+ lines.push('');
81
+ lines.push('### 3. Fix mínimo');
82
+ lines.push('');
83
+ lines.push(
84
+ 'Corrija a causa raiz e **apenas ela**. Um bug é o convite mais comum para refatoração ' +
85
+ 'oportunista: se você vir outros problemas no caminho, **anote-os no comentário final ' +
86
+ 'em vez de corrigi-los** — cada mudança extra aumenta o risco de uma correção que ' +
87
+ 'precisava ser cirúrgica.'
88
+ );
89
+ lines.push('');
90
+ lines.push('### 4. Teste de regressão');
91
+ lines.push('');
92
+ lines.push(
93
+ 'O teste da fase 1 agora **passa**. Garanta que ele fica no repositório e que **falharia ' +
94
+ 'de novo** se o fix fosse revertido — essa é a única prova de que ele testa o defeito, e ' +
95
+ 'não outra coisa. Rode a suíte inteira: um fix que quebra outro teste não está pronto.'
96
+ );
97
+
98
+ lines.push('');
99
+ lines.push('## Ao terminar');
100
+ lines.push('');
101
+ lines.push(
102
+ `1. Commit com a **causa raiz** na mensagem: \`fix: <o que estava errado> (#${bug.number})\`, ` +
103
+ 'e o corpo explicando a origem — não o sintoma.'
104
+ );
105
+ lines.push(`2. Abra o Pull Request com \`Fixes #${bug.number}\` no corpo.`);
106
+ lines.push(
107
+ `3. O board move sozinho: o Bug sai de **${STAGE_DEVELOPMENT}** (${PROGRESS_IN_PROGRESS}) ` +
108
+ `para **${STAGE_CODE_REVIEW}** quando o PR abre. Não mova à mão.`
109
+ );
110
+
111
+ if (bugDoc && bugDoc.trim()) {
112
+ lines.push('');
113
+ lines.push('---');
114
+ lines.push('');
115
+ lines.push('## bug.md — investigação já registrada');
116
+ lines.push('');
117
+ lines.push(
118
+ '> Escrito por IA **sem executar código**. Trate a causa raiz como hipótese a confirmar, ' +
119
+ 'não como fato.'
120
+ );
121
+ lines.push('');
122
+ lines.push(bugDoc.trim());
123
+ } else {
124
+ lines.push('');
125
+ lines.push('---');
126
+ lines.push('');
127
+ lines.push(
128
+ '> **Sem `bug.md`.** A investigação inteira é sua: o relato acima e os comentários são ' +
129
+ 'tudo o que existe.'
130
+ );
131
+ }
132
+
133
+ if (comments && comments.length > 0) {
134
+ lines.push('');
135
+ lines.push('## Comentários da issue');
136
+ lines.push('');
137
+ lines.push(
138
+ '> É onde costuma estar o que faltava no relato original — passos extras, ambiente, ' +
139
+ 'e o retorno de quem reportou. Em conflito, o comentário mais recente prevalece.'
140
+ );
141
+ for (const group of comments) {
142
+ for (const c of group.items || []) {
143
+ lines.push('');
144
+ lines.push(`**${c.author || 'desconhecido'}** (${c.createdAt}):`);
145
+ lines.push('');
146
+ lines.push(String(c.body || '').trim());
147
+ }
148
+ }
149
+ }
150
+
151
+ if (codeDigest) {
152
+ lines.push('');
153
+ lines.push('## Digest do código');
154
+ lines.push('');
155
+ lines.push(codeDigest);
156
+ }
157
+
158
+ lines.push('');
159
+ return lines.join('\n');
160
+ }
@@ -0,0 +1,51 @@
1
+ // Caminhos e checagem estrutural do bug.md — o artefato do Bug (RFC-004 §5).
2
+ //
3
+ // Fica fora de docs/features/ de propósito: um Bug NÃO é uma Feature e não gera
4
+ // spec.md/plan.md. Manter os dois na mesma pasta faria o `validate` e o
5
+ // `resolveFeaturePaths` da UI tropeçarem num diretório sem os arquivos que
6
+ // esperam.
7
+ //
8
+ // Decisão em função pura, I/O no comando — nada aqui toca o filesystem.
9
+ import path from 'node:path';
10
+ import { slugify } from './slugify.mjs';
11
+
12
+ /**
13
+ * Caminhos do bug.md a partir do título da issue (função PURA).
14
+ *
15
+ * Devolve o relativo (links, mensagem de commit) e o absoluto ancorado na raiz
16
+ * do repositório (fs) — o config é procurado subindo na árvore, e os documentos
17
+ * moram junto dele.
18
+ *
19
+ * @param {string} title título da issue (com ou sem o prefixo [BUG])
20
+ * @param {string} [root] raiz do repositório; ausente = process.cwd()
21
+ * @returns {{ slug: string, dirRel: string, fileRel: string, dirAbs: string, fileAbs: string }}
22
+ */
23
+ export function bugDocPaths(title, root = null) {
24
+ const slug = slugify(title);
25
+ const dirRel = `docs/bugs/${slug}`;
26
+ const fileRel = `${dirRel}/bug.md`;
27
+ const base = root || process.cwd();
28
+ return {
29
+ slug,
30
+ dirRel,
31
+ fileRel,
32
+ dirAbs: path.resolve(base, dirRel),
33
+ fileAbs: path.resolve(base, fileRel),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Seções obrigatórias ausentes de um documento (função PURA).
39
+ *
40
+ * Extraída da duplicação que existia em validate.mjs (um laço idêntico para
41
+ * spec.md e outro para plan.md). Casa por `# <seção>`, o que aceita qualquer
42
+ * nível de heading (`##`, `###`) — a checagem é de PRESENÇA, não de hierarquia.
43
+ *
44
+ * @param {string} content conteúdo do documento
45
+ * @param {string[]} sections títulos obrigatórios
46
+ * @returns {string[]} os que faltam, na ordem declarada
47
+ */
48
+ export function findMissingSections(content, sections) {
49
+ const text = String(content || '');
50
+ return (sections || []).filter(section => !text.includes(`# ${section}`));
51
+ }
@@ -0,0 +1,81 @@
1
+ // Decisões da triagem de Bug (RFC-004 §4.1) — puras, testáveis, sem I/O.
2
+ //
3
+ // O portão que estas funções guardam: P2/P3 só entram na fila técnica com o
4
+ // bug.md aprovado. A razão não é burocracia — é que um bug sem causa raiz
5
+ // investigada consome o tempo do dev na investigação que a triagem deveria ter
6
+ // feito, e é aí que "corrigir o sintoma" acontece. P0/P1 são exceção porque
7
+ // esperar o documento custa mais que investigar durante a correção.
8
+
9
+ import { LABEL_BUG_APPROVED, LABEL_NEEDS_HUMAN, LABEL_CRITIQUE_FAILED } from '../config.mjs';
10
+
11
+ export const TRIAGE_ACTIONS = ['accept', 'reject', 'duplicate'];
12
+
13
+ // Severidades que dispensam o bug.md antes da fila.
14
+ const SEVERITY_WITHOUT_DOC = ['P0', 'P1'];
15
+
16
+ /**
17
+ * Resolve a ação de triagem informada (função PURA).
18
+ *
19
+ * Mesmo contrato de resolveStageName/resolveProgressName: devolve
20
+ * `{action, error}` em vez de lançar.
21
+ *
22
+ * @param {string} input
23
+ * @returns {{ action: string|null, error: string|null }}
24
+ */
25
+ export function resolveTriageAction(input) {
26
+ const key = String(input ?? '').trim().toLowerCase();
27
+ if (!key) {
28
+ return { action: null, error: `Informe a ação: ${TRIAGE_ACTIONS.join(', ')}.` };
29
+ }
30
+ const match = TRIAGE_ACTIONS.find(a => a === key);
31
+ return match
32
+ ? { action: match, error: null }
33
+ : {
34
+ action: null,
35
+ error: `Ação "${input}" não existe. Use uma de: ${TRIAGE_ACTIONS.join(', ')}.`,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * O bug pode ser aceito na fila técnica? (função PURA)
41
+ *
42
+ * @param {object} params
43
+ * @param {string|null} params.severity P0–P3
44
+ * @param {string[]} params.labels labels da issue
45
+ * @returns {{ ok: boolean, error: string|null }}
46
+ */
47
+ export function canAcceptBug({ severity = null, labels = [] } = {}) {
48
+ const names = labels || [];
49
+
50
+ // Portões humanos da crítica valem para qualquer severidade: aceitar um bug
51
+ // cujo documento foi reprovado é justamente o que o portão existe para evitar.
52
+ if (names.includes(LABEL_NEEDS_HUMAN)) {
53
+ return {
54
+ ok: false,
55
+ error:
56
+ `A crítica reprovou repetidas vezes e \`${LABEL_NEEDS_HUMAN}\` está aplicada. ` +
57
+ 'Revise o bug.md e remova a label antes de aceitar.',
58
+ };
59
+ }
60
+ if (names.includes(LABEL_CRITIQUE_FAILED)) {
61
+ return {
62
+ ok: false,
63
+ error:
64
+ `A crítica adversarial apontou problemas graves (\`${LABEL_CRITIQUE_FAILED}\`). ` +
65
+ 'Corrija o bug.md e remova a label antes de aceitar.',
66
+ };
67
+ }
68
+
69
+ if (SEVERITY_WITHOUT_DOC.includes(severity)) return { ok: true, error: null };
70
+
71
+ if (!names.includes(LABEL_BUG_APPROVED)) {
72
+ return {
73
+ ok: false,
74
+ error:
75
+ `Bug ${severity || 'sem severidade'} exige o bug.md validado antes da fila técnica. ` +
76
+ 'Aplique `spec-wave:bug` para gerá-lo e `spec-wave:ready` para validá-lo — ' +
77
+ 'ou reclassifique a severidade com --severity se for urgente.',
78
+ };
79
+ }
80
+ return { ok: true, error: null };
81
+ }