@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.
- package/README.md +1 -0
- package/bin/spec-wave.mjs +41 -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 +8 -0
- 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 +16 -3
- 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/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 +54 -4
- 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,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
|
+
}
|
package/src/commands/update.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|