@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
@@ -33,6 +33,7 @@ import { lintLanguage } from '../lib/output-lint.mjs';
33
33
  import { slugify } from '../lib/slugify.mjs';
34
34
  import { detectIssueType } from '../lib/issue-type.mjs';
35
35
  import { loadConfig } from '../lib/project-root.mjs';
36
+ import { loadPrompt, toolFreeSystemPrompt } from '../lib/prompt-loader.mjs';
36
37
  import {
37
38
  renderDecompositionDoc, parseDecompositionDoc, DECOMPOSITION_FILE,
38
39
  } from '../lib/decomposition-doc.mjs';
@@ -207,60 +208,17 @@ function commitFile(filePath, content, message) {
207
208
  git('git push');
208
209
  }
209
210
 
210
- const FEATURE_SYSTEM_PROMPT = `Você é um Tech Lead experiente em decomposição de trabalho ágil.
211
- A partir da Feature fornecida (com spec.md e plan.md), gere uma lista de Stories e Tasks.
212
-
213
- Responda APENAS com JSON válido neste formato:
214
- {
215
- "stories": [
216
- {
217
- "title": "Título curto da story (apenas a parte 'quero', sem prefixo)",
218
- "userStory": "Como <perfil>, quero <objetivo>, para <benefício>",
219
- "body": "Descrição complementar da story com contexto e critérios de aceite relevantes",
220
- "dependsOn": [0],
221
- "tasks": [
222
- {
223
- "title": "Título técnico curto da task (sem prefixo)",
224
- "body": "Descrição técnica detalhada"
225
- }
226
- ]
227
- }
228
- ]
229
- }
230
-
231
- Regras:
232
- - "title" deve ser CURTO (máx. ~60 caracteres): apenas a parte "quero" da user story, sem o "Como" nem o "para", e sem prefixo. Ex.: "visualizar meus repositórios em layout responsivo"
233
- - "userStory" deve trazer a user story completa no formato "Como <perfil>, quero <objetivo>, para <benefício>"
234
- - "body" é texto complementar (contexto, critérios de aceite); não repita o título
235
- - Cada Story deve ter 2–5 Tasks associadas
236
- - Tasks devem ser atividades técnicas concretas, com "title" curto e "body" detalhado
237
- - Gere entre 3 e 7 Stories por Feature
238
- - Ordene as stories na sequência de implementação — a ORDEM da lista importa
239
- - "dependsOn" (opcional): índices 0-based das stories ANTERIORES na lista das quais esta story depende. Referencie apenas índices menores que o da própria story. Use [] quando a story puder ser feita em paralelo (sem dependências); se omitido, assume-se dependência da story anterior (sequencial)`;
240
-
241
- const RFC_SYSTEM_PROMPT = `Você é um Tech Lead experiente. A partir do RFC fornecido (proposta técnica/de processo), gere a lista de Tasks técnicas concretas necessárias para implementá-lo.
242
-
243
- Responda APENAS com JSON válido neste formato:
244
- {
245
- "tasks": [
246
- {
247
- "title": "Título técnico curto da task (sem prefixo)",
248
- "body": "Descrição técnica detalhada (o que fazer, áreas/arquivos afetados, critério de pronto)"
249
- }
250
- ]
251
- }
252
-
253
- Regras:
254
- - "title" CURTO (máx. ~60 caracteres), sem prefixo.
255
- - "body" detalhado e acionável.
256
- - Gere entre 3 e 10 Tasks concretas que, juntas, cubram o RFC.`;
211
+ // Os prompts vivem em `src/plugin/skills/decompose/model-prompt.{feature,rfc}.md`
212
+ // (sobrescrevíveis em `.spec-wave/prompts/decompose.feature.md`). O contrato JSON é o
213
+ // `json.shape` do frontmatter — quem parseia a saída e quem instrui o modelo
214
+ // passam a ler a MESMA declaração, em vez de duas cópias que podiam divergir.
257
215
 
258
216
  // ---------------------------------------------------------------------------
259
217
  // Etapa 1: rascunho (label spec-wave:decompose)
260
218
  // ---------------------------------------------------------------------------
261
219
 
262
220
  async function draftDecomposition(ctx) {
263
- const { token, owner, repo, issue, issueNumber, type, labels, usage, docDir, docPath, docRel } = ctx;
221
+ const { token, owner, repo, issue, issueNumber, type, labels, usage, root, docDir, docPath, docRel } = ctx;
264
222
  const number = parseInt(issueNumber, 10);
265
223
  const kind = DECOMPOSE_TARGETS[type]; // Feature → 'stories'; RFC → 'tasks'
266
224
  const blobUrl = `https://github.com/${owner}/${repo}/blob/main/${docRel}`;
@@ -292,8 +250,12 @@ async function draftDecomposition(ctx) {
292
250
  `\n## plan.md\n${planContent || '(plan.md não encontrado)'}`,
293
251
  ].join('\n');
294
252
 
253
+ const decomposePrompt = loadPrompt(
254
+ type === 'RFC' ? 'decompose/rfc' : 'decompose/feature',
255
+ { cwd: root },
256
+ );
295
257
  const generated = parseModelJson(await generateDocument(
296
- type === 'RFC' ? RFC_SYSTEM_PROMPT : FEATURE_SYSTEM_PROMPT,
258
+ toolFreeSystemPrompt(decomposePrompt),
297
259
  userContent,
298
260
  { action: 'decompose', labels, usage }
299
261
  ));
@@ -19,7 +19,7 @@ import {
19
19
  import { homedir, tmpdir } from 'node:os';
20
20
  import path from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
- import { CONFIG_FILE } from '../config.mjs';
22
+ import { CONFIG_FILE, LABEL_DEV_AGENT } from '../config.mjs';
23
23
  import { findConfigPath } from '../lib/project-root.mjs';
24
24
 
25
25
  const __dir = path.dirname(fileURLToPath(import.meta.url));
@@ -82,7 +82,7 @@ export function renderAgentConfig({ owner, repo }) {
82
82
  # Schema completo: https://github.com/${AGENT_REPO}#configuração
83
83
 
84
84
  repo = "${owner}/${repo}"
85
- queue_label = "spec-wave:dev-agent"
85
+ queue_label = "${LABEL_DEV_AGENT}"
86
86
 
87
87
  # Defaults do agente (descomente para ajustar):
88
88
  #poll_interval_secs = 60 # consulta à fila quando ocioso
@@ -412,7 +412,7 @@ async function finishSetup({ cfg, home, binPath, configPath, options, configActi
412
412
  p.outro(
413
413
  `${chalk.green('✓')} Agente pronto.\n` +
414
414
  ` Rodar agora: ${chalk.cyan('spec-wave dev-agent --run')}\n` +
415
- ' Enfileirar: aplique a label spec-wave:dev-agent numa issue [FEATURE] já decomposta.'
415
+ ` Enfileirar: aplique a label ${LABEL_DEV_AGENT} numa issue [FEATURE] já decomposta.`
416
416
  );
417
417
  }
418
418
 
@@ -12,7 +12,8 @@ import { resolveToken, verifyTokenScopes } from '../api/auth.mjs';
12
12
  import { getProjectSnapshot } from '../api/github-graphql.mjs';
13
13
  import {
14
14
  CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER, STATUS_OPTIONS,
15
- ALL_LABELS, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX, DEFAULT_MAX_CRITIQUE_ATTEMPTS,
15
+ RETIRED_STAGES, ALL_LABELS, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX,
16
+ DEFAULT_MAX_CRITIQUE_ATTEMPTS, STAGE_TRACKS,
16
17
  } from '../config.mjs';
17
18
  import { findConfigPath } from '../lib/project-root.mjs';
18
19
  import {
@@ -284,7 +285,12 @@ async function checkConfig(ctx) {
284
285
  export function inspectBoardHygiene({ boardStages = null, repoLabels = null } = {}) {
285
286
  const canonical = STATUS_OPTIONS.map(s => s.name);
286
287
  const known = new Set(canonical);
287
- const unknownStages = boardStages ? boardStages.filter(s => !known.has(s)) : [];
288
+ const retired = new Map(RETIRED_STAGES.map(s => [s.name, s]));
289
+ const foreign = boardStages ? boardStages.filter(s => !known.has(s)) : [];
290
+ // Uma etapa aposentada também é "fora do fluxo", mas a orientação ao usuário
291
+ // é diferente da de uma coluna inventada — por isso saem em listas separadas.
292
+ const retiredStages = foreign.filter(s => retired.has(s)).map(s => retired.get(s));
293
+ const unknownStages = foreign.filter(s => !retired.has(s));
288
294
  const missingStages = boardStages ? canonical.filter(s => !boardStages.includes(s)) : [];
289
295
 
290
296
  const knownLabels = new Set(ALL_LABELS.map(l => l.name));
@@ -294,7 +300,29 @@ export function inspectBoardHygiene({ boardStages = null, repoLabels = null } =
294
300
  const missingLabels = repoLabels
295
301
  ? ALL_LABELS.map(l => l.name).filter(n => !repoLabels.includes(n))
296
302
  : [];
297
- return { unknownStages, missingStages, orphanLabels, missingLabels };
303
+ return { unknownStages, retiredStages, missingStages, orphanLabels, missingLabels };
304
+ }
305
+
306
+ /**
307
+ * O board comporta a trilha do Bug? (função PURA)
308
+ *
309
+ * Um Bug percorre 🐞 Triagem → ✅ Ready → … → 🚀 Deploy → 🎉 Done (RFC-004 §4).
310
+ * Se o board não tem alguma dessas colunas, o item não tem onde parar: o campo
311
+ * "Etapa" só aceita as opções que existem, então a escrita falha em silêncio e
312
+ * o bug fica sem etapa — invisível em toda tela que filtra por etapa.
313
+ *
314
+ * Só reporta quando há Bug aberto: um board sem bugs não precisa da trilha.
315
+ *
316
+ * @param {object} params
317
+ * @param {string[]|null} [params.boardStages] opções do campo Etapa
318
+ * @param {number} [params.openBugCount] bugs abertos no repositório
319
+ * @returns {{ missingTrackStages: string[] }}
320
+ */
321
+ export function inspectBugTrack({ boardStages = null, openBugCount = 0 } = {}) {
322
+ if (!boardStages || openBugCount <= 0) return { missingTrackStages: [] };
323
+ return {
324
+ missingTrackStages: STAGE_TRACKS.Bug.filter(s => !boardStages.includes(s)),
325
+ };
298
326
  }
299
327
 
300
328
  async function checkBoardHygiene(ctx) {
@@ -315,6 +343,7 @@ async function checkBoardHygiene(ctx) {
315
343
  }
316
344
 
317
345
  let repoLabels = null;
346
+ let openBugCount = 0;
318
347
  if (ctx.token && cfg.owner && cfg.repo) {
319
348
  try {
320
349
  const res = await makeOctokit(ctx.token)
@@ -323,9 +352,17 @@ async function checkBoardHygiene(ctx) {
323
352
  } catch {
324
353
  // labels não verificáveis agora — segue só com o board
325
354
  }
355
+ try {
356
+ const bugs = await makeOctokit(ctx.token).paginate('GET /repos/{owner}/{repo}/issues', {
357
+ owner: cfg.owner, repo: cfg.repo, labels: '[BUG]', state: 'open', per_page: 100,
358
+ });
359
+ openBugCount = bugs.filter(i => !i.pull_request).length;
360
+ } catch {
361
+ // sem acesso às issues — o check da trilha do Bug fica de fora
362
+ }
326
363
  }
327
364
 
328
- const { unknownStages, missingStages, orphanLabels, missingLabels } =
365
+ const { unknownStages, retiredStages, missingStages, orphanLabels, missingLabels } =
329
366
  inspectBoardHygiene({ boardStages, repoLabels });
330
367
 
331
368
  const notes = [];
@@ -343,10 +380,32 @@ async function checkBoardHygiene(ctx) {
343
380
  'Mova os itens para uma etapa do fluxo e remova a coluna.'
344
381
  );
345
382
  }
383
+ for (const stage of retiredStages) {
384
+ status = 'warn';
385
+ notes.push(
386
+ `Etapa descontinuada ainda no board: ${stage.name} (removida do fluxo na v${stage.removedIn}). ` +
387
+ `Mova os itens dela para ${stage.replacedBy} e apague a coluna nas configurações do campo ` +
388
+ '"Etapa"; depois rode `refresh --config` para tirar o id do .spec-wave.json.'
389
+ );
390
+ }
346
391
  if (missingStages.length > 0) {
347
392
  status = 'warn';
348
- notes.push(`Etapas do RFC-001 ausentes no board: ${missingStages.join(', ')} — rode \`refresh\`.`);
393
+ notes.push(
394
+ `Etapas do RFC-001 ausentes no board: ${missingStages.join(', ')} — rode ` +
395
+ '`refresh --stages --dry-run` para ver o plano, ou crie a coluna à mão nas configurações ' +
396
+ 'do campo "Etapa" e rode `refresh --config`.'
397
+ );
398
+ }
399
+ const { missingTrackStages } = inspectBugTrack({ boardStages, openBugCount });
400
+ if (missingTrackStages.length > 0) {
401
+ status = 'warn';
402
+ notes.push(
403
+ `${openBugCount} Bug(s) aberto(s), mas o board não tem: ${missingTrackStages.join(', ')}. ` +
404
+ 'Um Bug movido para uma etapa inexistente fica SEM Etapa e some das telas — ' +
405
+ 'rode `refresh --stages`.'
406
+ );
349
407
  }
408
+
350
409
  if (repoLabels) {
351
410
  if (orphanLabels.length > 0) {
352
411
  status = 'warn';
@@ -504,10 +563,22 @@ async function checkAi(ctx) {
504
563
  );
505
564
  } else {
506
565
  notes.push(
507
- `Saída estruturada da crítica: ${provider.value === 'anthropic' ? 'tool call forçado' : 'response_format json_schema'} ` +
566
+ `Saída estruturada da crítica: tool call forçado (${provider.value}) ` +
508
567
  `· strict=${supportsStrictSchema(critiqueModel) ? 'sim' : 'não'} neste modelo.`
509
568
  );
510
569
  }
570
+ // O backend anthropic não chama a API — sobe o Claude Code CLI como
571
+ // subprocesso, que não existe no runner dos workflows (só há setup-node).
572
+ if (provider.value === 'anthropic') {
573
+ problems.push(
574
+ 'O provider `anthropic` NÃO roda nos GitHub Actions: ele sobe o Claude Code CLI como ' +
575
+ 'subprocesso, ausente no runner. As Actions de spec/plan/decompose vão falhar. ' +
576
+ 'Troque para `"provider": "openrouter"` no .spec-wave.json (e adicione o secret ' +
577
+ 'OPENROUTER_API_KEY), ou instale o Claude Code no workflow e defina ' +
578
+ 'SPEC_WAVE_ALLOW_ANTHROPIC_IN_CI=1. Localmente o anthropic funciona.'
579
+ );
580
+ }
581
+
511
582
  if (process.env[provider.secret]) {
512
583
  notes.push(`${provider.secret} presente no ambiente local.`);
513
584
  } else {
@@ -0,0 +1,195 @@
1
+ // Gera o bug.md (RFC-004 §5) — espelho do generate-spec, com três diferenças
2
+ // que vêm da natureza do artefato:
3
+ //
4
+ // 1. O relato mora nos COMENTÁRIOS, não só no corpo. Nas origens "reprovação
5
+ // de QA" e "suporte", o corpo da issue é uma linha e o que descreve o
6
+ // defeito vem depois, em comentário. Por isso eles entram no payload.
7
+ // 2. A crítica adversarial recebe o relato original junto do documento: a
8
+ // pergunta que ela responde é se a causa raiz proposta explica OS SINTOMAS
9
+ // RELATADOS — sem o relato, ela só avalia coerência interna.
10
+ // 3. Escreve em docs/bugs/<slug>/, fora de docs/features/.
11
+ import { execSync } from 'node:child_process';
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import { resolveToken } from '../api/auth.mjs';
14
+ import {
15
+ getIssue, removeLabel, addLabel, commentOnIssue, listIssueComments,
16
+ } from '../api/github-rest.mjs';
17
+ import { generateDocument } from '../lib/claude.mjs';
18
+ import { recordUsage } from '../lib/usage-report.mjs';
19
+ import { loadConfig } from '../lib/project-root.mjs';
20
+ import { loadPrompt, toolFreeSystemPrompt } from '../lib/prompt-loader.mjs';
21
+ import { detectIssueType } from '../lib/issue-type.mjs';
22
+ import { bugDocPaths } from '../lib/bug-doc.mjs';
23
+ import {
24
+ runCritique, resolveCritiqueAttempt, renderNeedsHumanComment,
25
+ } from '../lib/critique.mjs';
26
+ import {
27
+ LABEL_BUG, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, REQUIRED_BUG_SECTIONS,
28
+ TARGET_LANGUAGE, DEFAULT_MAX_CRITIQUE_ATTEMPTS, labelNames,
29
+ } from '../config.mjs';
30
+
31
+ // Relato = corpo + comentários humanos, na ordem cronológica. Comentários do
32
+ // próprio spec-wave são ruído aqui (críticas anteriores, relatórios de uso):
33
+ // realimentá-los faria o modelo auditar a si mesmo em vez do defeito.
34
+ function buildReport(issue, comments) {
35
+ const parts = [`### Descrição da issue\n\n${issue.body || '(sem descrição)'}`];
36
+ const humanos = (comments || []).filter(c => !isSpecWaveComment(c.body));
37
+ for (const c of humanos) {
38
+ const autor = c.user?.login || 'desconhecido';
39
+ parts.push(`### Comentário de @${autor}\n\n${c.body}`);
40
+ }
41
+ return parts.join('\n\n');
42
+ }
43
+
44
+ function isSpecWaveComment(body) {
45
+ const t = String(body || '');
46
+ return t.includes('<!-- spec-wave:') || t.includes('**(spec-wave)**') || t.includes('(spec-wave)');
47
+ }
48
+
49
+ export async function generateBug({ issueNumber }) {
50
+ const token = await resolveToken();
51
+ const [owner, repo] = (process.env.GITHUB_REPOSITORY || '').split('/');
52
+ const { root } = loadConfig();
53
+ const n = parseInt(issueNumber, 10);
54
+
55
+ if (!owner || !repo) {
56
+ throw new Error(
57
+ 'GITHUB_REPOSITORY env var não definida.\n' +
58
+ 'Este comando roda no GitHub Actions. Para testar localmente:\n' +
59
+ ' GITHUB_REPOSITORY=owner/repo spec-wave generate-bug --issue-number 1'
60
+ );
61
+ }
62
+
63
+ console.log(`Buscando issue #${n}...`);
64
+ const issue = await getIssue(token, owner, repo, n);
65
+
66
+ // Guarda invertida em relação ao generate-spec: aqui só Bug passa.
67
+ const type = detectIssueType(issue);
68
+ if (type !== 'Bug') {
69
+ console.log(`Issue #${n} é ${type}: bug.md é gerado só para Bug.`);
70
+ await removeLabel(token, owner, repo, n, LABEL_BUG);
71
+ await commentOnIssue(
72
+ token, owner, repo, n,
73
+ `ℹ️ **bug.md não gerado:** o tipo **${type}** não usa esse artefato. ` +
74
+ 'Nenhum arquivo foi criado.'
75
+ ).catch(() => {});
76
+ return;
77
+ }
78
+
79
+ const issueLabels = labelNames(issue);
80
+ if (issueLabels.includes(LABEL_NEEDS_HUMAN)) {
81
+ console.log(`Issue #${n} está com ${LABEL_NEEDS_HUMAN}: geração bloqueada.`);
82
+ await removeLabel(token, owner, repo, n, LABEL_BUG);
83
+ return;
84
+ }
85
+
86
+ const { slug, dirRel, fileRel, dirAbs, fileAbs } = bugDocPaths(issue.title, root);
87
+
88
+ const comments = await listIssueComments(token, owner, repo, n).catch(() => []);
89
+ const report = buildReport(issue, comments);
90
+
91
+ const payload = {
92
+ metadata: {
93
+ bug_title: issue.title,
94
+ labels: issueLabels,
95
+ required_sections: REQUIRED_BUG_SECTIONS,
96
+ },
97
+ report: { raw: report },
98
+ };
99
+ const userContent =
100
+ `Gere o bug.md a partir deste payload JSON:\n\n${JSON.stringify(payload, null, 2)}`;
101
+
102
+ const usageEntries = [];
103
+ try {
104
+ console.log(`Gerando bug.md para: ${issue.title}`);
105
+ const systemPrompt = toolFreeSystemPrompt(loadPrompt('bug', { cwd: root }));
106
+ const { content } = await generateDocument(systemPrompt, userContent, {
107
+ action: 'bug',
108
+ labels: issueLabels,
109
+ lint: { lang: TARGET_LANGUAGE },
110
+ withReport: true,
111
+ usage: usageEntries,
112
+ });
113
+
114
+ mkdirSync(dirAbs, { recursive: true });
115
+ writeFileSync(fileAbs, content, 'utf-8');
116
+
117
+ const git = (cmd) => execSync(cmd, { stdio: 'inherit' });
118
+ git('git config user.email "spec-wave[bot]@github.com"');
119
+ git('git config user.name "spec-wave[bot]"');
120
+ git(`git add "${fileAbs}"`);
121
+ git(`git commit -m "docs: generate bug.md for ${slug} [spec-wave]"`);
122
+ git('git pull --rebase');
123
+ git('git push');
124
+
125
+ await removeLabel(token, owner, repo, n, LABEL_BUG);
126
+
127
+ // Crítica adversarial: NÃO fatal. Um bug.md gerado e criticado como grave
128
+ // ainda é melhor que nenhum — o portão é a label, verificada na triagem.
129
+ const critique = await critiqueBugDoc({
130
+ token, owner, repo, issueNumber: n, issue,
131
+ bugDoc: content, report, labels: issueLabels, cwd: root, usage: usageEntries,
132
+ });
133
+
134
+ await commentOnIssue(
135
+ token, owner, repo, n,
136
+ '🐞 **bug.md gerado automaticamente!**\n\n' +
137
+ `📄 Arquivo: [\`${fileRel}\`](https://github.com/${owner}/${repo}/blob/main/${fileRel})\n\n` +
138
+ 'Revise a **causa raiz** e o **teste de regressão** — são as duas seções que decidem se ' +
139
+ 'a correção ataca o defeito ou o sintoma. Quando estiver pronto, valide com:\n' +
140
+ `\`\`\`\ngh issue edit ${n} --add-label "spec-wave:ready"\n\`\`\`` +
141
+ (critique?.blocked ? '\n\n⛔ A crítica adversarial encontrou problemas graves (acima).' : '')
142
+ );
143
+
144
+ console.log(`bug.md criado em: ${fileRel} (dir: ${dirRel})`);
145
+ } catch (err) {
146
+ // Mesmo motivo do generate-spec: sem remover a label, re-aplicá-la não
147
+ // emite evento e a issue vira beco sem saída.
148
+ await removeLabel(token, owner, repo, n, LABEL_BUG).catch(() => {});
149
+ await commentOnIssue(
150
+ token, owner, repo, n,
151
+ '❌ **Falha ao gerar o bug.md**\n\n' +
152
+ `\`\`\`\n${err.message}\n\`\`\`\n\n` +
153
+ `A label \`${LABEL_BUG}\` foi removida para destravar o gatilho — ` +
154
+ 'adicione-a de novo para tentar outra vez.'
155
+ ).catch(() => {});
156
+ throw err;
157
+ } finally {
158
+ await recordUsage({ token, owner, repo, issueNumber: n, entries: usageEntries });
159
+ }
160
+ }
161
+
162
+ // Roda a crítica e reflete o veredito em labels/comentário. Best-effort: uma
163
+ // falha da crítica não invalida o documento já commitado.
164
+ async function critiqueBugDoc({
165
+ token, owner, repo, issueNumber, issue, bugDoc, report, labels, cwd, usage,
166
+ }) {
167
+ try {
168
+ const comments = await listIssueComments(token, owner, repo, issueNumber).catch(() => []);
169
+ const { attempt, blocked } = resolveCritiqueAttempt({
170
+ comments, labels, kind: 'bug', maxAttempts: DEFAULT_MAX_CRITIQUE_ATTEMPTS,
171
+ });
172
+
173
+ if (blocked) {
174
+ await addLabel(token, owner, repo, issueNumber, LABEL_NEEDS_HUMAN);
175
+ await commentOnIssue(
176
+ token, owner, repo, issueNumber,
177
+ renderNeedsHumanComment({ kind: 'bug', maxAttempts: DEFAULT_MAX_CRITIQUE_ATTEMPTS })
178
+ );
179
+ return { blocked: true };
180
+ }
181
+
182
+ const result = await runCritique({
183
+ kind: 'bug', bugDoc, bugReport: report, attempt, labels, usage, cwd,
184
+ });
185
+ await commentOnIssue(token, owner, repo, issueNumber, result.markdown);
186
+ if (result.grave) {
187
+ await addLabel(token, owner, repo, issueNumber, LABEL_CRITIQUE_FAILED);
188
+ return { blocked: true };
189
+ }
190
+ return { blocked: false };
191
+ } catch (err) {
192
+ console.error(`Crítica do bug.md falhou (não fatal): ${err.message}`);
193
+ return { blocked: false };
194
+ }
195
+ }
@@ -18,6 +18,7 @@ import { recordUsage } from '../lib/usage-report.mjs';
18
18
  import { slugify } from '../lib/slugify.mjs';
19
19
  import { loadConfig, resolveFromRoot } from '../lib/project-root.mjs';
20
20
  import { buildTechContext } from '../lib/tech-context.mjs';
21
+ import { loadPrompt, toolFreeSystemPrompt } from '../lib/prompt-loader.mjs';
21
22
 
22
23
  // Aviso anexado ao comentário quando o lint de idioma ainda reprova após o
23
24
  // retry automático do generateDocument (excertos ao redor de cada vazamento).
@@ -30,25 +31,9 @@ function formatLintWarning(lintFindings) {
30
31
  return `\n\n⚠️ possíveis artefatos de idioma no documento: ${excerpts}`;
31
32
  }
32
33
 
33
- const SYSTEM_PROMPT = `Você é um Tech Lead experiente. Gere um plano técnico (plan.md) completo e detalhado, baseado ESTRITAMENTE no spec.md fornecido.
34
-
35
- O plano deve conter EXATAMENTE estas seções em português, nesta ordem:
36
- # Estratégia Técnica
37
- - Abordagem Arquitetural, Decisões-Chave e uma Matriz de Rastreabilidade (tabela) ligando cada Critério de Aceite do spec a um componente técnico.
38
- # Detalhamento da Implementação
39
- - 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.
40
- - Subseções: ## Backend, ## Banco de Dados, ## Frontend, ## Infraestrutura.
41
- # Segurança e Conformidade
42
- # Estratégia de Testes
43
- - Unitários, Integração e E2E.
44
- # Rollback e Monitoramento
45
- - Plano de Rollback, Métricas Observadas e Alertas.
46
-
47
- Regras OBRIGATÓRIAS:
48
- - TODA mudança de banco, endpoint de API ou componente de UI DEVE referenciar um Critério de Aceite específico do spec.md (rastreabilidade).
49
- - Use APENAS as tecnologias e serviços listados no tech_context fornecido. Não invente APIs ou serviços inexistentes.
50
- - Forneça detalhes acionáveis: caminhos exatos de endpoints, nomes de DTOs, constraints de banco.
51
- - Responda APENAS com o conteúdo do plan.md, sem texto adicional.`;
34
+ // O prompt vive em `src/plugin/skills/plan/model-prompt.md` (sobrescrevível pelo
35
+ // projeto em `.spec-wave/prompts/plan.md`). `toolFreeSystemPrompt` remove a orientação que
36
+ // assume Read/Glob/Grep: aqui a geração é UMA chamada, sem tool loop.
52
37
 
53
38
  /**
54
39
  * Roda a crítica do plan e aplica as labels de bloqueio.
@@ -186,7 +171,8 @@ export async function generatePlan({ issueNumber }) {
186
171
  const usageEntries = [];
187
172
  try {
188
173
  console.log(`Gerando plan.md para: ${issue.title}`);
189
- const { content, lintFindings } = await generateDocument(SYSTEM_PROMPT, userContent, {
174
+ const systemPrompt = toolFreeSystemPrompt(loadPrompt('plan', { cwd: root }));
175
+ const { content, lintFindings } = await generateDocument(systemPrompt, userContent, {
190
176
  action: 'plan',
191
177
  labels: issueLabels,
192
178
  lint: { lang: TARGET_LANGUAGE },
@@ -7,6 +7,7 @@ import { generateDocument } from '../lib/claude.mjs';
7
7
  import { recordUsage } from '../lib/usage-report.mjs';
8
8
  import { slugify } from '../lib/slugify.mjs';
9
9
  import { loadConfig, resolveFromRoot } from '../lib/project-root.mjs';
10
+ import { loadPrompt, toolFreeSystemPrompt } from '../lib/prompt-loader.mjs';
10
11
  import { detectIssueType } from '../lib/issue-type.mjs';
11
12
  import {
12
13
  allowsSpecPlan, SPEC_PLAN_EXCLUDED_TYPES, TARGET_LANGUAGE, labelNames,
@@ -23,27 +24,9 @@ function formatLintWarning(lintFindings) {
23
24
  return `\n\n⚠️ possíveis artefatos de idioma no documento: ${excerpts}`;
24
25
  }
25
26
 
26
- const SYSTEM_PROMPT = `Você é um Product Manager experiente. Gere uma especificação funcional (spec.md) completa para a Feature descrita pelo usuário.
27
-
28
- O spec deve conter EXATAMENTE estas seções em português, nesta ordem:
29
- # Visão Geral
30
- - Objetivo, Personas e Critérios de Sucesso como bullets.
31
- # Regras de Negócio
32
- # Fluxos
33
- - Subseções: ## Fluxo Principal (Happy Path), ## Fluxos Alternativos, ## Cenários de Erro.
34
- - 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.
35
- - 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.
36
- # Critérios de Aceite
37
- - OBRIGATORIAMENTE no formato Gherkin, dentro de um bloco \`\`\`gherkin com Given/When/Then. Um cenário por critério.
38
- # Dependências
39
- - Subdivida em Internas e Externas.
40
- # Requisitos Não-Funcionais
41
- - Performance, Segurança e Usabilidade.
42
-
43
- Regras:
44
- - NÃO invente regras de negócio. Se faltar informação, marque explicitamente com "[TODO: requer esclarecimento do PO]".
45
- - Seja específico e detalhado em cada seção.
46
- - Responda APENAS com o conteúdo do spec.md, sem texto adicional.`;
27
+ // O prompt vive em `src/plugin/skills/spec/model-prompt.md` (sobrescrevível pelo
28
+ // projeto em `.spec-wave/prompts/spec.md`). `toolFreeSystemPrompt` remove a orientação que assume
29
+ // Read/Glob/Grep: aqui a geração é UMA chamada de completions, sem tool loop.
47
30
 
48
31
  export async function generateSpec({ issueNumber }) {
49
32
  const token = await resolveToken();
@@ -103,7 +86,8 @@ export async function generateSpec({ issueNumber }) {
103
86
  const usageEntries = [];
104
87
  try {
105
88
  console.log(`Gerando spec.md para: ${issue.title}`);
106
- const { content, lintFindings } = await generateDocument(SYSTEM_PROMPT, userContent, {
89
+ const systemPrompt = toolFreeSystemPrompt(loadPrompt('spec', { cwd: root }));
90
+ const { content, lintFindings } = await generateDocument(systemPrompt, userContent, {
107
91
  action: 'spec',
108
92
  labels: issueLabels,
109
93
  lint: { lang: TARGET_LANGUAGE },