@spec-wave/cli 0.14.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.
- package/README.md +1 -0
- package/bin/spec-wave.mjs +44 -2
- package/package.json +8 -2
- package/src/agent/anthropic-agent.mjs +337 -0
- package/src/agent/errors.mjs +33 -0
- package/src/agent/index.mjs +108 -0
- package/src/agent/openrouter-agent.mjs +378 -0
- package/src/agent/run-types.mjs +59 -0
- package/src/agent/telemetry.mjs +54 -0
- package/src/agent/tools.mjs +452 -0
- package/src/agent/tracing.mjs +106 -0
- package/src/api/github-rest.mjs +206 -2
- package/src/commands/bug.mjs +8 -0
- package/src/commands/code-review.mjs +45 -4
- package/src/commands/decompose.mjs +11 -49
- package/src/commands/dev-agent.mjs +3 -3
- package/src/commands/doctor.mjs +77 -6
- package/src/commands/generate-bug.mjs +195 -0
- package/src/commands/generate-plan.mjs +6 -20
- package/src/commands/generate-spec.mjs +6 -22
- package/src/commands/implement.mjs +105 -2
- package/src/commands/init.mjs +3 -3
- package/src/commands/install-skill.mjs +72 -16
- package/src/commands/issue.mjs +9 -7
- package/src/commands/move.mjs +11 -1
- package/src/commands/qa.mjs +23 -2
- package/src/commands/refresh.mjs +145 -5
- package/src/commands/triage.mjs +174 -0
- package/src/commands/update.mjs +352 -62
- package/src/commands/validate.mjs +82 -10
- package/src/config.mjs +159 -1
- package/src/lib/bug-context.mjs +160 -0
- package/src/lib/bug-doc.mjs +51 -0
- package/src/lib/bug-triage.mjs +81 -0
- package/src/lib/claude.mjs +71 -254
- package/src/lib/critique.mjs +43 -30
- package/src/lib/implement-board.mjs +12 -1
- package/src/lib/plugin-skills.mjs +122 -0
- package/src/lib/pr-branch.mjs +267 -0
- package/src/lib/prompt-loader.mjs +257 -0
- package/src/lib/skill-file.mjs +35 -0
- package/src/plugin/.claude-plugin/plugin.json +20 -0
- package/src/plugin/README.md +73 -0
- package/src/plugin/skills/bug/SKILL.md +60 -0
- package/src/plugin/skills/bug/model-prompt.critique.md +48 -0
- package/src/plugin/skills/bug/model-prompt.md +74 -0
- package/src/plugin/skills/decompose/SKILL.md +111 -0
- package/src/plugin/skills/decompose/model-prompt.critique.md +46 -0
- package/src/plugin/skills/decompose/model-prompt.feature.md +69 -0
- package/src/plugin/skills/decompose/model-prompt.rfc.md +52 -0
- package/src/plugin/skills/doctor/SKILL.md +51 -0
- package/src/plugin/skills/fix-pr/SKILL.md +130 -0
- package/src/plugin/skills/implement/SKILL.md +102 -0
- package/src/plugin/skills/info/SKILL.md +40 -0
- package/src/plugin/skills/issue/SKILL.md +63 -0
- package/src/plugin/skills/move/SKILL.md +52 -0
- package/src/plugin/skills/order/SKILL.md +36 -0
- package/src/plugin/skills/plan/SKILL.md +53 -0
- package/src/plugin/skills/plan/model-prompt.critique.md +44 -0
- package/src/plugin/skills/plan/model-prompt.md +59 -0
- package/src/plugin/skills/plan/reference/tech-context.md +56 -0
- package/src/plugin/skills/ready/SKILL.md +44 -0
- package/src/plugin/skills/rfc/SKILL.md +47 -0
- package/src/plugin/skills/setup/SKILL.md +67 -0
- package/src/plugin/skills/spec/SKILL.md +37 -0
- package/src/plugin/skills/spec/model-prompt.md +61 -0
- package/src/plugin/skills/story/SKILL.md +49 -0
- package/src/plugin/skills/task/SKILL.md +41 -0
- package/src/plugin/skills/triage/SKILL.md +52 -0
- package/src/plugin/skills/uninstall/SKILL.md +43 -0
- package/src/plugin/skills/update/SKILL.md +51 -0
- package/src/plugin/skills/workflow/SKILL.md +154 -0
- package/src/templates/skill/SKILL.md +69 -7
- package/src/templates/workflows/generate-bug.yml +36 -0
- package/src/templates/workflows/validate.yml +2 -1
- package/src/ui/wizard.mjs +5 -2
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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 },
|
|
@@ -6,11 +6,13 @@ import path from 'node:path';
|
|
|
6
6
|
import { resolveToken } from '../api/auth.mjs';
|
|
7
7
|
import {
|
|
8
8
|
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_DONE, STAGE_ORDER,
|
|
9
|
-
PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE,
|
|
9
|
+
PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE, labelNames,
|
|
10
10
|
} from '../config.mjs';
|
|
11
11
|
import { getIssue, listIssueComments, listBlockedBy } from '../api/github-rest.mjs';
|
|
12
12
|
import { listSubIssues, getIssueParent, addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
13
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
14
|
+
import { bugDocPaths } from '../lib/bug-doc.mjs';
|
|
15
|
+
import { buildBugContext } from '../lib/bug-context.mjs';
|
|
14
16
|
import { slugify } from '../lib/slugify.mjs';
|
|
15
17
|
import { parseDependencies, orderStories, formatDependencyLine } from '../lib/dependencies.mjs';
|
|
16
18
|
import { loadProjectConfig, resolveField } from '../lib/board.mjs';
|
|
@@ -360,6 +362,102 @@ function renderCommand(template, vars) {
|
|
|
360
362
|
// Modo Feature: avalia as Stories da Feature (dependências + Etapa no board),
|
|
361
363
|
// pula as já implementadas (Code Review+) e monta UM contexto único com todas
|
|
362
364
|
// as pendentes em ordem topológica — spec-kit acionado uma vez.
|
|
365
|
+
/**
|
|
366
|
+
* Modo Bug (RFC-004 §7.1): sem tasks, sem spec/plan, sem Feature-pai a arrastar.
|
|
367
|
+
*
|
|
368
|
+
* O bug.md, quando existe, entra como HIPÓTESE — foi escrito por IA sem
|
|
369
|
+
* executar código, e o contexto diz isso explicitamente ao executor. Quando não
|
|
370
|
+
* existe (o caso do P0, que dispensa o documento), o contexto assume a
|
|
371
|
+
* investigação inteira.
|
|
372
|
+
*/
|
|
373
|
+
async function implementBug({ token, owner, repo, config, bug, dryRun, repoRoot }) {
|
|
374
|
+
const issueNumber = bug.number;
|
|
375
|
+
const severity = (labelNames(bug).find(n => /^P[0-3]$/.test(n))) || null;
|
|
376
|
+
|
|
377
|
+
// Item afetado (best-effort): dá ao executor o contexto do que quebrou. O pai
|
|
378
|
+
// de um Bug pode ser Feature OU Story — o tipo vem do prefixo do título.
|
|
379
|
+
let parent = null;
|
|
380
|
+
try {
|
|
381
|
+
const p0 = await getIssueParent(token, bug.node_id);
|
|
382
|
+
if (p0?.number) {
|
|
383
|
+
parent = { number: p0.number, title: p0.title, kind: detectIssueType(p0) || 'Item' };
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// sem pai legível — bug órfão é caso previsto (reporte de suporte)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// bug.md do repositório, se já foi gerado.
|
|
390
|
+
const { fileAbs, fileRel } = bugDocPaths(bug.title, repoRoot);
|
|
391
|
+
let bugDoc = null;
|
|
392
|
+
if (existsSync(fileAbs)) {
|
|
393
|
+
bugDoc = readFileSync(fileAbs, 'utf-8');
|
|
394
|
+
p.log.info(`bug.md encontrado em ${chalk.cyan(fileRel)}.`);
|
|
395
|
+
} else {
|
|
396
|
+
p.log.warn(`Sem bug.md em ${fileRel} — o contexto assume a investigação inteira.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Board: Bug → Desenvolvimento (In Progress). Não move a Feature-pai.
|
|
400
|
+
await applyBoardMoves({
|
|
401
|
+
token,
|
|
402
|
+
moves: planBoardMoves('start', { bug: { nodeId: bug.node_id, number: issueNumber } }),
|
|
403
|
+
cwd: repoRoot || process.cwd(),
|
|
404
|
+
dryRun,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const comments = [];
|
|
408
|
+
const all = await listIssueComments(token, owner, repo, issueNumber).catch(() => []);
|
|
409
|
+
if (all.length > 0) {
|
|
410
|
+
const items = all.slice(-MAX_COMMENTS_PER_ISSUE).map(c => ({
|
|
411
|
+
...c,
|
|
412
|
+
body: c.body.length > MAX_COMMENT_CHARS
|
|
413
|
+
? `${c.body.slice(0, MAX_COMMENT_CHARS)}…[truncado]`
|
|
414
|
+
: c.body,
|
|
415
|
+
}));
|
|
416
|
+
comments.push({ issueNumber, kind: 'Bug', total: all.length, items });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let codeDigest = null;
|
|
420
|
+
try {
|
|
421
|
+
codeDigest = await buildCodeDigest({ sinceIso: bug.created_at || null, paths: [] });
|
|
422
|
+
} catch {
|
|
423
|
+
codeDigest = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const blockedByWarnings = [];
|
|
427
|
+
try {
|
|
428
|
+
const blocked = await listBlockedBy(token, owner, repo, issueNumber).catch(() => []);
|
|
429
|
+
for (const b of blocked) {
|
|
430
|
+
if (b?.state !== 'closed') blockedByWarnings.push(`#${b.number} — ${b.title}`);
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// dependências não legíveis — segue
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const context = buildBugContext({
|
|
437
|
+
bug: { number: issueNumber, title: bug.title, body: bug.body || '' },
|
|
438
|
+
bugDoc, parent, comments, codeDigest, blockedByWarnings, severity,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await writeContextAndRunSpecKit({
|
|
442
|
+
config,
|
|
443
|
+
issueNumber,
|
|
444
|
+
type: 'Bug',
|
|
445
|
+
title: bug.title,
|
|
446
|
+
specPlan: { spec: null, plan: null, specPath: null, planPath: null },
|
|
447
|
+
context,
|
|
448
|
+
dryRun,
|
|
449
|
+
outroSuccess: `Bug #${issueNumber} corrigido — abra o PR com \`Fixes #${issueNumber}\`.`,
|
|
450
|
+
onSuccess: async () => {
|
|
451
|
+
await applyBoardMoves({
|
|
452
|
+
token,
|
|
453
|
+
moves: planBoardMoves('success', { bug: { nodeId: bug.node_id, number: issueNumber } }),
|
|
454
|
+
cwd: repoRoot || process.cwd(),
|
|
455
|
+
dryRun,
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
363
461
|
async function implementFeature({ token, owner, repo, config, feature, featureDirOpt, dryRun, repoRoot }) {
|
|
364
462
|
// F1. Stories (sub-issues) da Feature.
|
|
365
463
|
const subs = await listSubIssues(token, feature.node_id).catch(() => []);
|
|
@@ -630,9 +728,14 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
630
728
|
// Modo Feature: Stories pendentes em ordem de dependência, contexto único.
|
|
631
729
|
await implementFeature({ token, owner, repo, config, feature: issue, featureDirOpt, dryRun, repoRoot });
|
|
632
730
|
return;
|
|
731
|
+
} else if (type === 'Bug') {
|
|
732
|
+
// Modo Bug: sem tasks e sem spec/plan — o trabalho é investigar antes de
|
|
733
|
+
// corrigir, e o contexto impõe essa ordem.
|
|
734
|
+
await implementBug({ token, owner, repo, config, bug: issue, dryRun, repoRoot });
|
|
735
|
+
return;
|
|
633
736
|
} else {
|
|
634
737
|
p.log.error(
|
|
635
|
-
`implement
|
|
738
|
+
`implement aceita Feature, Story, Task ou Bug. Issue #${issueNumber} é do tipo ${type || 'desconhecido'}.`
|
|
636
739
|
);
|
|
637
740
|
process.exitCode = 1;
|
|
638
741
|
return;
|
package/src/commands/init.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { setupProject } from '../setup/project.mjs';
|
|
|
9
9
|
import { setupLabels } from '../setup/labels.mjs';
|
|
10
10
|
import { setupFiles } from '../setup/files.mjs';
|
|
11
11
|
import { getFileContent } from '../api/github-rest.mjs';
|
|
12
|
-
import { CONFIG_FILE, AI_PROVIDERS, getProvider, DEFAULT_PROVIDER, PORTAL_URL, WORKFLOW_FILES, ISSUE_TEMPLATE_FILES } from '../config.mjs';
|
|
12
|
+
import { CONFIG_FILE, AI_PROVIDERS, getProvider, DEFAULT_PROVIDER, PORTAL_URL, WORKFLOW_FILES, ISSUE_TEMPLATE_FILES, STATUS_OPTIONS, ALL_LABELS } from '../config.mjs';
|
|
13
13
|
|
|
14
14
|
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const pkg = JSON.parse(readFileSync(path.join(__dir, '..', '..', 'package.json'), 'utf-8'));
|
|
@@ -143,7 +143,7 @@ export async function init(options) {
|
|
|
143
143
|
labelSpinner.start('Criando labels...');
|
|
144
144
|
try {
|
|
145
145
|
await setupLabels(token, owner, repo, labelSpinner);
|
|
146
|
-
labelSpinner.stop(
|
|
146
|
+
labelSpinner.stop(`Labels criadas (${ALL_LABELS.length} labels)`);
|
|
147
147
|
} catch (err) {
|
|
148
148
|
labelSpinner.stop('');
|
|
149
149
|
p.log.error(`Erro ao criar labels: ${err.message}`);
|
|
@@ -204,7 +204,7 @@ export async function init(options) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
p.note(
|
|
207
|
-
|
|
207
|
+
`As ${STATUS_OPTIONS.length} colunas do RFC-001 foram criadas no campo "Etapa".\n` +
|
|
208
208
|
'Para usá-las como colunas do board:\n' +
|
|
209
209
|
' 1. Abra o projeto no GitHub\n' +
|
|
210
210
|
' 2. Clique em "..." → "Settings" da view de Board\n' +
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import yaml from 'js-yaml';
|
|
4
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { homedir } from 'node:os';
|
|
7
6
|
import path from 'node:path';
|
|
7
|
+
import { parseSkill } from '../lib/skill-file.mjs';
|
|
8
|
+
import { listPluginSkills, planPluginSkillFiles } from '../lib/plugin-skills.mjs';
|
|
8
9
|
|
|
9
10
|
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
// Fonte única da skill, publicada via "files": ["src"] no package.json.
|
|
@@ -33,6 +34,17 @@ export const TARGETS = [
|
|
|
33
34
|
project: '.claude/skills/spec-wave/SKILL.md',
|
|
34
35
|
global: '.claude/skills/spec-wave/SKILL.md',
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
key: 'codex',
|
|
39
|
+
name: 'Codex CLI',
|
|
40
|
+
// Codex não tem conceito de plugin: ele varre diretórios de skills. Então o
|
|
41
|
+
// MESMO conteúdo do plugin é copiado skill a skill para `.agents/skills`,
|
|
42
|
+
// que é o caminho que o Codex lê no repo (e `~/.agents/skills` no usuário).
|
|
43
|
+
format: 'skills-dir',
|
|
44
|
+
detect: ['.codex', '.agents'],
|
|
45
|
+
project: path.join('.agents', 'skills'),
|
|
46
|
+
global: path.join('.agents', 'skills'),
|
|
47
|
+
},
|
|
36
48
|
{
|
|
37
49
|
key: 'opencode',
|
|
38
50
|
name: 'opencode',
|
|
@@ -86,22 +98,14 @@ export const TARGETS = [
|
|
|
86
98
|
|
|
87
99
|
const TARGET_BY_KEY = new Map(TARGETS.map((t) => [t.key, t]));
|
|
88
100
|
|
|
89
|
-
// Separa o frontmatter YAML do corpo do SKILL.md.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let meta = {};
|
|
94
|
-
try {
|
|
95
|
-
meta = yaml.load(match[1]) || {};
|
|
96
|
-
} catch {
|
|
97
|
-
meta = {};
|
|
98
|
-
}
|
|
99
|
-
return { meta, frontmatter: match[1], body: match[2].trim() };
|
|
100
|
-
}
|
|
101
|
+
// Separa o frontmatter YAML do corpo do SKILL.md. Implementação em
|
|
102
|
+
// `lib/skill-file.mjs` (compartilhada com o loader das skills de IA);
|
|
103
|
+
// re-exportada aqui porque `update.mjs` e os testes a importam deste módulo.
|
|
104
|
+
export { parseSkill };
|
|
101
105
|
|
|
102
106
|
// Banner de versão inserido no topo do corpo da skill instalada. O agente lê
|
|
103
107
|
// esta linha e, se `npx @spec-wave/cli@latest --version` for maior, orienta reinstalar.
|
|
104
|
-
function versionBanner(version) {
|
|
108
|
+
export function versionBanner(version) {
|
|
105
109
|
return (
|
|
106
110
|
`> ⚙️ **spec-wave skill v${version}** — esta skill é uma cópia estática. ` +
|
|
107
111
|
'Se `npx @spec-wave/cli@latest --version` indicar uma versão maior, ela está ' +
|
|
@@ -182,6 +186,20 @@ export function isDetected(target, baseDir) {
|
|
|
182
186
|
// 'ausente' | 'bloco ausente' | 'desatualizada' — ou null se está em dia com a
|
|
183
187
|
// versão empacotada na CLI. Compartilhado entre `update` e `info`.
|
|
184
188
|
export function skillCopyReason(dest, parsed) {
|
|
189
|
+
// `skills-dir` não é um arquivo só: o destino é um diretório com uma pasta
|
|
190
|
+
// por skill. Diretório inteiro ausente conta como 'ausente'; qualquer arquivo
|
|
191
|
+
// faltando ou divergente conta como 'desatualizada' (é o mesmo remédio —
|
|
192
|
+
// reinstalar —, então não vale distinguir os dois casos).
|
|
193
|
+
if (dest.format === 'skills-dir') {
|
|
194
|
+
if (!existsSync(dest.path)) return 'ausente';
|
|
195
|
+
const files = planPluginSkillFiles(dest.path, CLI_VERSION, versionBanner);
|
|
196
|
+
if (!files.length) return null;
|
|
197
|
+
const stale = files.some(
|
|
198
|
+
(f) => !existsSync(f.path) || readFileSync(f.path, 'utf-8') !== f.content,
|
|
199
|
+
);
|
|
200
|
+
return stale ? 'desatualizada' : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
185
203
|
const desired = renderContent(dest.format, parsed, CLI_VERSION);
|
|
186
204
|
const existing = existsSync(dest.path) ? readFileSync(dest.path, 'utf-8') : null;
|
|
187
205
|
if (existing === null) return 'ausente';
|
|
@@ -312,7 +330,12 @@ export async function installSkill(options = {}) {
|
|
|
312
330
|
if (options.dryRun) {
|
|
313
331
|
p.note(
|
|
314
332
|
jobs
|
|
315
|
-
.map((j) =>
|
|
333
|
+
.map((j) => {
|
|
334
|
+
const head = `${chalk.dim(j.target.name.padEnd(20))} ${j.dest.path} ${chalk.dim(`(${j.dest.format})`)}`;
|
|
335
|
+
if (j.dest.format !== 'skills-dir') return head;
|
|
336
|
+
const names = listPluginSkills().map((s) => s.name);
|
|
337
|
+
return `${head}\n${chalk.dim(` ${names.length} skills: ${names.join(', ')}`)}`;
|
|
338
|
+
})
|
|
316
339
|
.join('\n'),
|
|
317
340
|
`Dry-run — nada será gravado (escopo: ${scopeLabel})`,
|
|
318
341
|
);
|
|
@@ -323,6 +346,34 @@ export async function installSkill(options = {}) {
|
|
|
323
346
|
// 4) Gravar cada destino.
|
|
324
347
|
const written = [];
|
|
325
348
|
for (const { target, dest } of jobs) {
|
|
349
|
+
// `skills-dir` grava N arquivos (uma pasta por skill do plugin) em vez de
|
|
350
|
+
// um. Sobrescrever aqui é seguro sem confirmar por arquivo: o destino é um
|
|
351
|
+
// diretório inteiro do spec-wave, não um arquivo que o usuário mantém.
|
|
352
|
+
if (dest.format === 'skills-dir') {
|
|
353
|
+
const files = planPluginSkillFiles(dest.path, pkg.version, versionBanner);
|
|
354
|
+
if (!files.length) {
|
|
355
|
+
p.log.error(`${target.name}: nenhuma skill encontrada no plugin empacotado — pulado.`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (existsSync(dest.path) && !options.force && !options.yes) {
|
|
359
|
+
const ok = await p.confirm({
|
|
360
|
+
message: `${target.name}: ${dest.path} já existe. Sobrescrever as skills do spec-wave?`,
|
|
361
|
+
initialValue: true,
|
|
362
|
+
});
|
|
363
|
+
if (p.isCancel(ok) || !ok) {
|
|
364
|
+
p.log.info(`${target.name}: mantido (não sobrescrito).`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const file of files) {
|
|
369
|
+
mkdirSync(path.dirname(file.path), { recursive: true });
|
|
370
|
+
writeFileSync(file.path, file.content, 'utf-8');
|
|
371
|
+
}
|
|
372
|
+
const count = new Set(files.map((f) => f.skill)).size;
|
|
373
|
+
written.push({ target, dest, count });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
326
377
|
const content =
|
|
327
378
|
dest.format === 'agents'
|
|
328
379
|
? mergeAgentsFile(dest.path, renderContent('agents', parsed, pkg.version))
|
|
@@ -352,7 +403,12 @@ export async function installSkill(options = {}) {
|
|
|
352
403
|
}
|
|
353
404
|
|
|
354
405
|
p.note(
|
|
355
|
-
written
|
|
406
|
+
written
|
|
407
|
+
.map((w) => {
|
|
408
|
+
const suffix = w.count ? chalk.dim(` (${w.count} skills)`) : '';
|
|
409
|
+
return `${chalk.green('✓')} ${chalk.bold(w.target.name)}${suffix}\n ${chalk.dim(w.dest.path)}`;
|
|
410
|
+
})
|
|
411
|
+
.join('\n'),
|
|
356
412
|
`Skill v${pkg.version} instalada (escopo: ${scopeLabel})`,
|
|
357
413
|
);
|
|
358
414
|
p.outro(
|
package/src/commands/issue.mjs
CHANGED
|
@@ -2,13 +2,11 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { resolveToken } from '../api/auth.mjs';
|
|
5
|
-
import { CONFIG_FILE,
|
|
5
|
+
import { CONFIG_FILE, PRIORITY_LABELS, WORK_ITEM_TYPES, initialStageForType } from '../config.mjs';
|
|
6
6
|
import { findConfigPath } from '../lib/project-root.mjs';
|
|
7
7
|
import { createIssue, getIssue } from '../api/github-rest.mjs';
|
|
8
8
|
import { addProjectItem, setItemSingleSelect, getSingleSelectField, addSubIssue } from '../api/github-graphql.mjs';
|
|
9
9
|
|
|
10
|
-
// Etapa inicial de todo work item recém-criado (📥 Backlog).
|
|
11
|
-
const INITIAL_STAGE = STATUS_OPTIONS[0].name;
|
|
12
10
|
const VALID_PRIORITIES = PRIORITY_LABELS.map(l => l.name);
|
|
13
11
|
|
|
14
12
|
// Resolve o tipo informado (case-insensitive) para o nome canônico (ex.: "Feature").
|
|
@@ -160,18 +158,22 @@ export async function issue(options) {
|
|
|
160
158
|
return;
|
|
161
159
|
}
|
|
162
160
|
|
|
161
|
+
// Etapa de nascimento: Backlog para quase todo tipo, 🐞 Triagem para Bug
|
|
162
|
+
// (✅ Ready quando o Bug já nasce P0). Ver initialStageForType.
|
|
163
|
+
const initialStage = initialStageForType(type, options.priority || null);
|
|
164
|
+
|
|
163
165
|
const boardSpinner = p.spinner();
|
|
164
166
|
boardSpinner.start('Adicionando ao Project...');
|
|
165
167
|
try {
|
|
166
168
|
const itemId = await addProjectItem(token, project.id, created.nodeId);
|
|
167
169
|
|
|
168
|
-
const stageOk = await setField(token, project, itemId, 'Etapa',
|
|
170
|
+
const stageOk = await setField(token, project, itemId, 'Etapa', initialStage);
|
|
169
171
|
const typeOk = await setField(token, project, itemId, 'Work Item Type', type);
|
|
170
172
|
if (options.priority) await setField(token, project, itemId, 'Priority', options.priority);
|
|
171
173
|
if (options.area) await setField(token, project, itemId, 'Area', options.area);
|
|
172
174
|
|
|
173
|
-
boardSpinner.stop(`Adicionada ao Project${stageOk ? ` em "${
|
|
174
|
-
if (!stageOk) p.log.warn(`Não foi possível definir a Etapa "${
|
|
175
|
+
boardSpinner.stop(`Adicionada ao Project${stageOk ? ` em "${initialStage}"` : ''}.`);
|
|
176
|
+
if (!stageOk) p.log.warn(`Não foi possível definir a Etapa "${initialStage}" (campo não encontrado).`);
|
|
175
177
|
if (!typeOk) p.log.warn(`Não foi possível definir o Work Item Type "${type}" (campo não encontrado).`);
|
|
176
178
|
} catch (err) {
|
|
177
179
|
boardSpinner.stop('');
|
|
@@ -185,7 +187,7 @@ export async function issue(options) {
|
|
|
185
187
|
Epic: `Próximo: crie Features sob este Epic com \`spec-wave feature --parent ${created.number} --title "..."\`.`,
|
|
186
188
|
};
|
|
187
189
|
p.outro(
|
|
188
|
-
`${chalk.green('✓')} ${type} #${created.number} criado em "${
|
|
190
|
+
`${chalk.green('✓')} ${type} #${created.number} criado em "${initialStage}"${parentLine}.\n` +
|
|
189
191
|
` ${hints[type] || ''}`
|
|
190
192
|
);
|
|
191
193
|
}
|