@spec-wave/cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +1 -0
  2. package/bin/spec-wave.mjs +41 -2
  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-rest.mjs +8 -0
  13. package/src/commands/bug.mjs +8 -0
  14. package/src/commands/code-review.mjs +45 -4
  15. package/src/commands/decompose.mjs +11 -49
  16. package/src/commands/dev-agent.mjs +3 -3
  17. package/src/commands/doctor.mjs +77 -6
  18. package/src/commands/generate-bug.mjs +195 -0
  19. package/src/commands/generate-plan.mjs +6 -20
  20. package/src/commands/generate-spec.mjs +6 -22
  21. package/src/commands/implement.mjs +105 -2
  22. package/src/commands/init.mjs +3 -3
  23. package/src/commands/install-skill.mjs +72 -16
  24. package/src/commands/issue.mjs +9 -7
  25. package/src/commands/move.mjs +11 -1
  26. package/src/commands/qa.mjs +23 -2
  27. package/src/commands/refresh.mjs +145 -5
  28. package/src/commands/triage.mjs +174 -0
  29. package/src/commands/update.mjs +16 -3
  30. package/src/commands/validate.mjs +82 -10
  31. package/src/config.mjs +159 -1
  32. package/src/lib/bug-context.mjs +160 -0
  33. package/src/lib/bug-doc.mjs +51 -0
  34. package/src/lib/bug-triage.mjs +81 -0
  35. package/src/lib/claude.mjs +71 -254
  36. package/src/lib/critique.mjs +43 -30
  37. package/src/lib/implement-board.mjs +12 -1
  38. package/src/lib/plugin-skills.mjs +122 -0
  39. package/src/lib/prompt-loader.mjs +257 -0
  40. package/src/lib/skill-file.mjs +35 -0
  41. package/src/plugin/.claude-plugin/plugin.json +20 -0
  42. package/src/plugin/README.md +73 -0
  43. package/src/plugin/skills/bug/SKILL.md +60 -0
  44. package/src/plugin/skills/bug/model-prompt.critique.md +48 -0
  45. package/src/plugin/skills/bug/model-prompt.md +74 -0
  46. package/src/plugin/skills/decompose/SKILL.md +111 -0
  47. package/src/plugin/skills/decompose/model-prompt.critique.md +46 -0
  48. package/src/plugin/skills/decompose/model-prompt.feature.md +69 -0
  49. package/src/plugin/skills/decompose/model-prompt.rfc.md +52 -0
  50. package/src/plugin/skills/doctor/SKILL.md +51 -0
  51. package/src/plugin/skills/fix-pr/SKILL.md +130 -0
  52. package/src/plugin/skills/implement/SKILL.md +102 -0
  53. package/src/plugin/skills/info/SKILL.md +40 -0
  54. package/src/plugin/skills/issue/SKILL.md +63 -0
  55. package/src/plugin/skills/move/SKILL.md +52 -0
  56. package/src/plugin/skills/order/SKILL.md +36 -0
  57. package/src/plugin/skills/plan/SKILL.md +53 -0
  58. package/src/plugin/skills/plan/model-prompt.critique.md +44 -0
  59. package/src/plugin/skills/plan/model-prompt.md +59 -0
  60. package/src/plugin/skills/plan/reference/tech-context.md +56 -0
  61. package/src/plugin/skills/ready/SKILL.md +44 -0
  62. package/src/plugin/skills/rfc/SKILL.md +47 -0
  63. package/src/plugin/skills/setup/SKILL.md +67 -0
  64. package/src/plugin/skills/spec/SKILL.md +37 -0
  65. package/src/plugin/skills/spec/model-prompt.md +61 -0
  66. package/src/plugin/skills/story/SKILL.md +49 -0
  67. package/src/plugin/skills/task/SKILL.md +41 -0
  68. package/src/plugin/skills/triage/SKILL.md +52 -0
  69. package/src/plugin/skills/uninstall/SKILL.md +43 -0
  70. package/src/plugin/skills/update/SKILL.md +51 -0
  71. package/src/plugin/skills/workflow/SKILL.md +154 -0
  72. package/src/templates/skill/SKILL.md +54 -4
  73. package/src/templates/workflows/generate-bug.yml +36 -0
  74. package/src/templates/workflows/validate.yml +2 -1
  75. package/src/ui/wizard.mjs +5 -2
@@ -0,0 +1,174 @@
1
+ // Triagem de Bug pela CLI (RFC-004 §4.1) — o mesmo desfecho que a tela do PM
2
+ // oferece, para quem trabalha no terminal ou automatiza.
3
+ //
4
+ // Três saídas, e nenhuma delas é "editar": aceitar (→ ✅ Ready), rejeitar
5
+ // (fecha) e duplicar (fecha, apontando a original). Corrigir o relato é
6
+ // conversa na issue.
7
+ //
8
+ // Rejeitar e duplicar NÃO mexem na Etapa: ela nunca retrocede, e um bug
9
+ // rejeitado não avançou para lugar nenhum — quem o tira das filas é o estado
10
+ // `closed` da issue.
11
+ import * as p from '@clack/prompts';
12
+ import chalk from 'chalk';
13
+ import { resolveToken } from '../api/auth.mjs';
14
+ import {
15
+ getIssue, addLabel, removeLabel, commentOnIssue, setIssueState,
16
+ } from '../api/github-rest.mjs';
17
+ import { detectIssueType } from '../lib/issue-type.mjs';
18
+ import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
19
+ import { loadConfig } from '../lib/project-root.mjs';
20
+ import { resolveTriageAction, canAcceptBug } from '../lib/bug-triage.mjs';
21
+ import {
22
+ CONFIG_FILE, STAGE_READY, PROGRESS_TODO, PRIORITY_LABELS,
23
+ LABEL_TRIAGED, LABEL_DUPLICATE, LABEL_WONT_FIX, labelNames,
24
+ } from '../config.mjs';
25
+
26
+ const PRIORITIES = PRIORITY_LABELS.map(l => l.name);
27
+
28
+ export async function triage({ action: actionArg, issue: issueArg, reason, of, severity }) {
29
+ const { action, error: actionError } = resolveTriageAction(actionArg);
30
+ if (actionError) {
31
+ p.log.error(actionError);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ const issueNumber = parseInt(String(issueArg).replace('#', ''), 10);
37
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
38
+ p.log.error(`Issue inválida: "${issueArg}". Use o número da issue, ex.: 42 ou #42.`);
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ if (severity && !PRIORITIES.includes(severity)) {
43
+ p.log.error(`Severidade inválida: ${severity}. Use uma de: ${PRIORITIES.join(', ')}.`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+
48
+ const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
49
+ const { config, root } = loadConfig();
50
+ const owner = envOwner || config?.owner;
51
+ const repo = envRepo || config?.repo;
52
+ if (!owner || !repo) {
53
+ p.log.error(
54
+ 'Não foi possível determinar owner/repo.\n' +
55
+ `Rode dentro de um repositório com ${CONFIG_FILE} ou defina GITHUB_REPOSITORY=owner/repo.`
56
+ );
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ let token;
62
+ try {
63
+ token = await resolveToken();
64
+ } catch (err) {
65
+ p.log.error(err.message);
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ p.intro(chalk.bold(`spec-wave triage ${action} #${issueNumber}`));
71
+
72
+ let issue;
73
+ try {
74
+ issue = await getIssue(token, owner, repo, issueNumber);
75
+ } catch (err) {
76
+ p.log.error(`Não foi possível ler a issue #${issueNumber}: ${err.message}`);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+
81
+ if (detectIssueType(issue) !== 'Bug') {
82
+ p.log.error(`#${issueNumber} não é um Bug — a triagem só se aplica a defeitos.`);
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+
87
+ const labels = labelNames(issue);
88
+ const severidadeAtual = severity || labels.find(n => PRIORITIES.includes(n)) || null;
89
+
90
+ if (action === 'accept') {
91
+ const { ok, error } = canAcceptBug({ severity: severidadeAtual, labels });
92
+ if (!ok) {
93
+ p.log.error(error);
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+
98
+ // Reclassificação: troca a label em vez de acumular. Duas prioridades na
99
+ // mesma issue tornam a ordem da fila indefinida.
100
+ if (severity) {
101
+ for (const outra of PRIORITIES) {
102
+ if (outra !== severity) await removeLabel(token, owner, repo, issueNumber, outra).catch(() => {});
103
+ }
104
+ await addLabel(token, owner, repo, issueNumber, severity);
105
+ p.log.info(`Severidade → ${severity}.`);
106
+ }
107
+
108
+ await addLabel(token, owner, repo, issueNumber, LABEL_TRIAGED);
109
+ await commentOnIssue(token, owner, repo, issueNumber,
110
+ '**Triado:** aceito para a fila técnica.').catch(() => {});
111
+ await moveToReady({ token, issueNumber, issue, root });
112
+
113
+ p.outro(`${chalk.green('✓')} Bug #${issueNumber} aceito → ${STAGE_READY}.`);
114
+ return;
115
+ }
116
+
117
+ if (action === 'reject') {
118
+ if (!reason || !reason.trim()) {
119
+ p.log.error('Rejeitar exige --reason "<motivo>".');
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+ await addLabel(token, owner, repo, issueNumber, LABEL_WONT_FIX);
124
+ await addLabel(token, owner, repo, issueNumber, LABEL_TRIAGED);
125
+ await commentOnIssue(token, owner, repo, issueNumber,
126
+ `**Rejeitado na triagem:** ${reason.trim()}`).catch(() => {});
127
+ await setIssueState(token, owner, repo, issueNumber, 'closed');
128
+ p.outro(`${chalk.green('✓')} Bug #${issueNumber} rejeitado e fechado.`);
129
+ return;
130
+ }
131
+
132
+ // duplicate
133
+ const original = parseInt(String(of || '').replace('#', ''), 10);
134
+ if (!Number.isInteger(original) || original <= 0) {
135
+ p.log.error('Marcar como duplicata exige --of <número da issue original>.');
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ if (original === issueNumber) {
140
+ p.log.error('Uma issue não pode ser duplicata de si mesma.');
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+
145
+ await addLabel(token, owner, repo, issueNumber, LABEL_DUPLICATE);
146
+ await addLabel(token, owner, repo, issueNumber, LABEL_TRIAGED);
147
+ await commentOnIssue(token, owner, repo, issueNumber, `**Duplicata de #${original}.**`).catch(() => {});
148
+ // Comentar também na original: sem isso, quem acompanha #original não fica
149
+ // sabendo que há outro relato do mesmo defeito — e o contexto extra está lá.
150
+ await commentOnIssue(token, owner, repo, original,
151
+ `#${issueNumber} foi marcada como duplicata desta issue.`).catch(() => {});
152
+ await setIssueState(token, owner, repo, issueNumber, 'closed');
153
+
154
+ p.outro(`${chalk.green('✓')} Bug #${issueNumber} marcado como duplicata de #${original} e fechado.`);
155
+ }
156
+
157
+ async function moveToReady({ token, issueNumber, issue, root }) {
158
+ const { project, error } = loadProjectConfig({ cwd: root || process.cwd() });
159
+ if (error) {
160
+ p.log.warn(`${error} — board não atualizado.`);
161
+ return;
162
+ }
163
+ try {
164
+ const etapaField = await resolveField(token, project, 'Etapa').catch(() => null);
165
+ const statusField = await resolveField(token, project, 'Status').catch(() => null);
166
+ const moved = await advanceToStage(
167
+ token, project, etapaField, statusField, issue.node_id, STAGE_READY, PROGRESS_TODO);
168
+ if (!moved) {
169
+ p.log.info(`#${issueNumber} já está em ${STAGE_READY} ou etapa posterior — mantido.`);
170
+ }
171
+ } catch (err) {
172
+ p.log.warn(`Falha ao mover no board: ${err.message}`);
173
+ }
174
+ }
@@ -20,8 +20,9 @@ import {
20
20
  import { readTemplate } from '../lib/templates.mjs';
21
21
  import {
22
22
  TARGETS, SKILL_SOURCE, CLI_VERSION, parseSkill, renderContent,
23
- mergeAgentsFile, resolveDest, isDetected, skillCopyReason,
23
+ mergeAgentsFile, resolveDest, isDetected, skillCopyReason, versionBanner,
24
24
  } from './install-skill.mjs';
25
+ import { planPluginSkillFiles } from '../lib/plugin-skills.mjs';
25
26
  import { findConfigPath } from '../lib/project-root.mjs';
26
27
 
27
28
  // Arquivos do repo gerenciados pela CLI (comparados com o template empacotado).
@@ -39,15 +40,27 @@ function detectSkill(parsed, baseDir, isGlobal) {
39
40
  const dest = resolveDest(target, baseDir, isGlobal);
40
41
  if (!dest) continue;
41
42
  const reason = skillCopyReason(dest, parsed);
42
- if (reason) {
43
- jobs.push({ target, dest, desired: renderContent(dest.format, parsed, CLI_VERSION), reason });
43
+ if (!reason) continue;
44
+ // `skills-dir` (Codex) não tem um `desired` único: são N arquivos, um por
45
+ // skill do plugin. O plano é montado na hora de aplicar.
46
+ if (dest.format === 'skills-dir') {
47
+ jobs.push({ target, dest, reason });
48
+ continue;
44
49
  }
50
+ jobs.push({ target, dest, desired: renderContent(dest.format, parsed, CLI_VERSION), reason });
45
51
  }
46
52
  return jobs;
47
53
  }
48
54
 
49
55
  // Aplica a atualização de uma skill (grava o arquivo / faz merge no AGENTS.md).
50
56
  function applySkill(job) {
57
+ if (job.dest.format === 'skills-dir') {
58
+ for (const file of planPluginSkillFiles(job.dest.path, CLI_VERSION, versionBanner)) {
59
+ mkdirSync(path.dirname(file.path), { recursive: true });
60
+ writeFileSync(file.path, file.content, 'utf-8');
61
+ }
62
+ return;
63
+ }
51
64
  const content = job.dest.format === 'agents'
52
65
  ? mergeAgentsFile(job.dest.path, job.desired)
53
66
  : job.desired;
@@ -4,12 +4,81 @@ import { resolveToken } from '../api/auth.mjs';
4
4
  import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
5
5
  import { slugify } from '../lib/slugify.mjs';
6
6
  import {
7
- CONFIG_FILE, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN,
8
- REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS, labelNames,
7
+ CONFIG_FILE, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_BUG, LABEL_BUG_APPROVED,
8
+ REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS, REQUIRED_BUG_SECTIONS, labelNames,
9
9
  } from '../config.mjs';
10
10
  import { findIncompleteDocSigns } from '../lib/doc-completeness.mjs';
11
+ import { bugDocPaths, findMissingSections } from '../lib/bug-doc.mjs';
12
+ import { detectIssueType } from '../lib/issue-type.mjs';
11
13
  import { loadConfig } from '../lib/project-root.mjs';
12
14
 
15
+ /**
16
+ * Validação do bug.md (RFC-004 §5).
17
+ *
18
+ * Mesma forma da validação de Feature — seções obrigatórias + sinais de
19
+ * documento truncado + portões humanos da crítica — com duas diferenças:
20
+ * um único arquivo, e a falha NÃO devolve o item para a etapa de spec (Bug não
21
+ * tem etapa de spec). Reaplicar `spec-wave:bug` é o caminho de retomada.
22
+ */
23
+ async function validateBug({ token, owner, repo, issue, issueNumber, root }) {
24
+ const n = parseInt(issueNumber, 10);
25
+ const { fileRel, fileAbs } = bugDocPaths(issue.title, root);
26
+ const errors = [];
27
+
28
+ const names = labelNames(issue);
29
+ const critiqueFailed = names.includes(LABEL_CRITIQUE_FAILED);
30
+ const needsHuman = names.includes(LABEL_NEEDS_HUMAN);
31
+ if (critiqueFailed) {
32
+ errors.push(
33
+ '🔎 A crítica adversarial apontou contradições GRAVES no `bug.md` (veja o comentário na ' +
34
+ `issue). Corrija e remova a label \`${LABEL_CRITIQUE_FAILED}\`.`
35
+ );
36
+ }
37
+ if (needsHuman) {
38
+ errors.push(
39
+ `🛑 A crítica reprovou repetidas vezes e a label \`${LABEL_NEEDS_HUMAN}\` foi aplicada. ` +
40
+ 'Uma pessoa precisa revisar o `bug.md` e remover a label.'
41
+ );
42
+ }
43
+
44
+ if (!existsSync(fileAbs)) {
45
+ errors.push(
46
+ `❌ \`bug.md\` não encontrado em \`${fileRel}\` — aplique \`${LABEL_BUG}\` para gerá-lo.`
47
+ );
48
+ } else {
49
+ const content = readFileSync(fileAbs, 'utf-8');
50
+ for (const section of findMissingSections(content, REQUIRED_BUG_SECTIONS)) {
51
+ errors.push(`❌ Seção obrigatória ausente no bug.md: **${section}**`);
52
+ }
53
+ for (const problem of findIncompleteDocSigns(content)) {
54
+ errors.push(`❌ \`bug.md\` parece incompleto: ${problem}`);
55
+ }
56
+ }
57
+
58
+ await removeLabel(token, owner, repo, n, 'spec-wave:ready');
59
+
60
+ if (errors.length > 0) {
61
+ await commentOnIssue(
62
+ token, owner, repo, n,
63
+ '⚠️ **Validação falhou — o bug.md não está pronto.**\n\n' +
64
+ errors.join('\n') +
65
+ `\n\nCorrija os problemas e adicione novamente a label \`spec-wave:ready\`.`
66
+ );
67
+ console.error('Validação falhou:', errors.join(', '));
68
+ process.exit(1);
69
+ }
70
+
71
+ await addLabel(token, owner, repo, n, LABEL_BUG_APPROVED);
72
+ await commentOnIssue(
73
+ token, owner, repo, n,
74
+ '✅ **bug.md validado.**\n\n' +
75
+ `📄 [\`${fileRel}\`](https://github.com/${owner}/${repo}/blob/main/${fileRel})\n\n` +
76
+ 'As seis seções obrigatórias estão presentes — reprodução, causa raiz e teste de ' +
77
+ 'regressão inclusive. O bug pode ser aceito na triagem.'
78
+ );
79
+ console.log('bug.md validado.');
80
+ }
81
+
13
82
  export async function validate({ issueNumber }) {
14
83
  const token = await resolveToken();
15
84
  const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
@@ -25,6 +94,13 @@ export async function validate({ issueNumber }) {
25
94
  }
26
95
 
27
96
  const issue = await getIssue(token, owner, repo, parseInt(issueNumber, 10));
97
+
98
+ // Bug tem artefato próprio (bug.md) e caminho próprio de validação — não passa
99
+ // pelo par spec.md + plan.md, que é exclusivo de Feature.
100
+ if (detectIssueType(issue) === 'Bug') {
101
+ return await validateBug({ token, owner, repo, issue, issueNumber, root });
102
+ }
103
+
28
104
  const slug = slugify(issue.title);
29
105
  // Ancorado na RAIZ do repo, não no cwd: rodar de um subdiretório encontra o
30
106
  // config subindo na árvore e precisa encontrar os documentos no mesmo lugar.
@@ -57,10 +133,8 @@ export async function validate({ issueNumber }) {
57
133
  errors.push('❌ `plan.md` não encontrado em `' + `${featureRel}/plan.md` + '`');
58
134
  } else {
59
135
  const planContent = readFileSync(planPath, 'utf-8');
60
- for (const section of REQUIRED_PLAN_SECTIONS) {
61
- if (!planContent.includes(`# ${section}`)) {
62
- errors.push(`❌ Seção obrigatória ausente no plan.md: **${section}**`);
63
- }
136
+ for (const section of findMissingSections(planContent, REQUIRED_PLAN_SECTIONS)) {
137
+ errors.push(`❌ Seção obrigatória ausente no plan.md: **${section}**`);
64
138
  }
65
139
  for (const problem of findIncompleteDocSigns(planContent)) {
66
140
  errors.push(`❌ \`plan.md\` parece incompleto: ${problem}`);
@@ -73,10 +147,8 @@ export async function validate({ issueNumber }) {
73
147
  errors.push('❌ `spec.md` não encontrado em `' + `${featureRel}/spec.md` + '`');
74
148
  } else {
75
149
  const specContent = readFileSync(specPath, 'utf-8');
76
- for (const section of REQUIRED_SPEC_SECTIONS) {
77
- if (!specContent.includes(`# ${section}`)) {
78
- errors.push(`❌ Seção obrigatória ausente no spec.md: **${section}**`);
79
- }
150
+ for (const section of findMissingSections(specContent, REQUIRED_SPEC_SECTIONS)) {
151
+ errors.push(`❌ Seção obrigatória ausente no spec.md: **${section}**`);
80
152
  }
81
153
  // Seções presentes não garantem documento completo: um corte dentro da
82
154
  // última seção passa na checagem acima (foi o caso da EP2-F13).
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',