@spec-wave/cli 0.24.0 → 0.26.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 +84 -0
- package/bin/spec-wave.mjs +4 -341
- package/package.json +1 -1
- package/src/api/github-rest.mjs +63 -1
- package/src/cli.mjs +400 -0
- package/src/commands/decompose.mjs +23 -16
- package/src/commands/doctor.mjs +32 -0
- package/src/commands/generate-bug.mjs +8 -14
- package/src/commands/generate-plan.mjs +2 -1
- package/src/commands/generate-spec.mjs +2 -1
- package/src/commands/mode.mjs +180 -0
- package/src/commands/run.mjs +491 -0
- package/src/commands/validate.mjs +22 -6
- package/src/config.mjs +4 -1
- package/src/lib/config-file.mjs +62 -0
- package/src/lib/doc-paths.mjs +51 -0
- package/src/lib/execution-mode.mjs +132 -0
- package/src/lib/next-step.mjs +412 -0
- package/src/lib/pr-step.mjs +102 -0
- package/src/lib/repo-links.mjs +84 -0
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/doctor/SKILL.md +1 -0
- package/src/plugin/skills/run/SKILL.md +76 -0
- package/src/plugin/skills/spec/SKILL.md +1 -1
- package/src/templates/skill/SKILL.md +24 -1
- package/src/templates/workflows/code-review.yml +5 -1
- package/src/templates/workflows/critique.yml +4 -0
- package/src/templates/workflows/decompose.yml +4 -0
- package/src/templates/workflows/generate-bug.yml +4 -0
- package/src/templates/workflows/generate-plan.yml +4 -0
- package/src/templates/workflows/generate-spec.yml +4 -0
- package/src/templates/workflows/qa.yml +6 -1
- package/src/templates/workflows/validate.yml +4 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Superfície da CLI: monta o programa do Commander e o devolve SEM executá-lo.
|
|
2
|
+
//
|
|
3
|
+
// Vive aqui, e não no `bin/`, porque o `bin/` executa (`parse()`) — importá-lo
|
|
4
|
+
// para introspecção dispararia a CLI. Com a montagem separada, o gerador da
|
|
5
|
+
// documentação (`apps/docs/scripts/generate-reference.mjs`) e os testes leem
|
|
6
|
+
// `program.commands` diretamente, em vez de parsear o texto do `--help`.
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(readFileSync(path.join(__dir, '..', 'package.json'), 'utf-8'));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Monta o programa do Commander com todos os comandos (função sem efeito).
|
|
18
|
+
*
|
|
19
|
+
* NÃO chama `.parse()` — quem executa é o `bin/spec-wave.mjs`.
|
|
20
|
+
*
|
|
21
|
+
* @returns {Command}
|
|
22
|
+
*/
|
|
23
|
+
export function buildProgram() {
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('spec-wave')
|
|
28
|
+
.description('Setup spec-driven GitHub workflow with Projects v2')
|
|
29
|
+
.version(pkg.version)
|
|
30
|
+
// Conta do `gh` a usar nesta execução. Existe porque um GH_TOKEN exportado no
|
|
31
|
+
// shell (um `.envrc` na raiz de um diretório de projetos, por exemplo) vale
|
|
32
|
+
// para repositórios de QUALQUER org abaixo dele — e o GraphQL do GitHub
|
|
33
|
+
// mascara o 403 resultante como "Could not resolve to a Repository".
|
|
34
|
+
.option('--account <login>', 'Conta do gh a usar (vence GITHUB_TOKEN/GH_TOKEN fora do CI)')
|
|
35
|
+
.hook('preAction', async (thisCommand) => {
|
|
36
|
+
const { account } = thisCommand.opts();
|
|
37
|
+
if (account) {
|
|
38
|
+
const { setAccountOverride } = await import('./api/auth.mjs');
|
|
39
|
+
setAccountOverride(account);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('init')
|
|
45
|
+
.description('Configura spec-wave em um repositório GitHub')
|
|
46
|
+
.option('--dry-run', 'Simula a configuração sem fazer alterações')
|
|
47
|
+
.option('--repo <owner/repo>', 'Repositório GitHub (ignora o wizard interativo)')
|
|
48
|
+
.option('--project-title <title>', 'Nome do GitHub Project (padrão: "<repo> — Spec Wave")')
|
|
49
|
+
.option('--skip-project', 'Pula a criação do GitHub Project (use se já foi criado)')
|
|
50
|
+
.option('--skip-labels', 'Pula a criação das labels')
|
|
51
|
+
.option('--skip-files', 'Pula a criação dos arquivos de workflow')
|
|
52
|
+
.option('--provider <provider>', 'Provider de IA dos workflows: anthropic ou openrouter')
|
|
53
|
+
.option('--model <model>', 'Modelo de IA usado pelos workflows (ex.: anthropic/claude-3.7-sonnet)')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
const { init } = await import('./commands/init.mjs');
|
|
56
|
+
await init(options);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('info')
|
|
61
|
+
.description('Mostra se o repositório atual foi inicializado e os dados do .spec-wave.json')
|
|
62
|
+
.option('--json', 'Saída em JSON (para uso programático)')
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const { info } = await import('./commands/info.mjs');
|
|
65
|
+
await info(options);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('refresh')
|
|
70
|
+
.description('Atualiza o .spec-wave.json local com os dados atuais do GitHub Project')
|
|
71
|
+
.option('--config', 'Re-consulta o Project e reescreve o .spec-wave.json')
|
|
72
|
+
.option('--stages', 'Acrescenta ao campo "Etapa" as colunas canônicas que faltam (nunca remove)')
|
|
73
|
+
.option('--dry-run', 'Com --stages: mostra o que seria enviado e não escreve nada')
|
|
74
|
+
.option('--yes', 'Com --stages: não pede confirmação')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
const { refresh } = await import('./commands/refresh.mjs');
|
|
77
|
+
await refresh(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('issue')
|
|
82
|
+
.description('Cria um work item (initiative/epic/feature/story/task...), opcionalmente como sub-issue, e adiciona ao board')
|
|
83
|
+
.requiredOption('--title <title>', 'Título (sem o prefixo de tipo, ex.: [FEATURE])')
|
|
84
|
+
.option('--type <type>', 'Tipo: initiative, epic, feature, story, task, bug, spike ou rfc', 'feature')
|
|
85
|
+
.option('--parent <n>', 'Número da issue pai (cria como sub-issue dela)')
|
|
86
|
+
.option('--body <text>', 'Descrição')
|
|
87
|
+
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3')
|
|
88
|
+
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
const { issue } = await import('./commands/issue.mjs');
|
|
91
|
+
await issue(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('initiative')
|
|
96
|
+
.description('Atalho de `issue --type initiative` (nó raiz que agrupa Epics)')
|
|
97
|
+
.requiredOption('--title <title>', 'Título da initiative (sem o prefixo [INITIATIVE])')
|
|
98
|
+
.option('--body <text>', 'Descrição da initiative')
|
|
99
|
+
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
100
|
+
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
101
|
+
.action(async (options) => {
|
|
102
|
+
const { initiative } = await import('./commands/initiative.mjs');
|
|
103
|
+
await initiative(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
program
|
|
107
|
+
.command('feature')
|
|
108
|
+
.description('Atalho de `issue --type feature`')
|
|
109
|
+
.requiredOption('--title <title>', 'Título da feature (sem o prefixo [FEATURE])')
|
|
110
|
+
.option('--parent <n>', 'Número da Epic pai (cria como sub-issue dela)')
|
|
111
|
+
.option('--body <text>', 'Descrição da feature')
|
|
112
|
+
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
113
|
+
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
114
|
+
.action(async (options) => {
|
|
115
|
+
const { feature } = await import('./commands/feature.mjs');
|
|
116
|
+
await feature(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('bug')
|
|
121
|
+
.description('Cria um Bug (atalho de `issue --type bug`) — nasce em 🐞 Triagem, ou ✅ Ready se P0')
|
|
122
|
+
.requiredOption('--title <title>', 'Título (sem o prefixo [BUG])')
|
|
123
|
+
.option('--parent <n>', 'Feature ou Story afetada (cria como sub-issue dela)')
|
|
124
|
+
.option('--body <text>', 'Descrição: passos, esperado e obtido')
|
|
125
|
+
.option('--priority <p>', 'Severidade: P0, P1, P2 ou P3')
|
|
126
|
+
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
127
|
+
.action(async (options) => {
|
|
128
|
+
const { bug } = await import('./commands/bug.mjs');
|
|
129
|
+
await bug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('triage')
|
|
134
|
+
.description('Tria um Bug: accept (→ ✅ Ready), reject (fecha) ou duplicate (fecha)')
|
|
135
|
+
.argument('<action>', 'accept | reject | duplicate')
|
|
136
|
+
.argument('<issue>', 'Número da issue do Bug')
|
|
137
|
+
.option('--reason <texto>', 'Motivo da rejeição (obrigatório em reject)')
|
|
138
|
+
.option('--of <n>', 'Número da issue original (obrigatório em duplicate)')
|
|
139
|
+
.option('--severity <p>', 'Reclassifica a severidade ao aceitar: P0–P3')
|
|
140
|
+
.action(async (action, issue, options) => {
|
|
141
|
+
const { triage } = await import('./commands/triage.mjs');
|
|
142
|
+
await triage({ action, issue, ...options })
|
|
143
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
program
|
|
147
|
+
.command('run')
|
|
148
|
+
.description('Executa LOCALMENTE o próximo passo do fluxo (o que a label dispararia no Actions)')
|
|
149
|
+
.argument('[issue]', 'Número da issue (Feature, Bug ou RFC)')
|
|
150
|
+
.option('--pr <n>', 'Modo PR: decide entre code-review e qa pelo estado das reviews')
|
|
151
|
+
.option('--dry-run', 'Decide e explica sem executar nada')
|
|
152
|
+
.option('--yes', 'Confirma o passo que exige confirmação')
|
|
153
|
+
.option('--apply', 'Autoriza especificamente o decompose-apply (erra se o passo pendente for outro)')
|
|
154
|
+
.option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug')
|
|
155
|
+
.option('--max-steps <n>', 'Encadeia até N passos (padrão: 1)', '1')
|
|
156
|
+
.option('--force', 'Ignora o portão de label de gatilho pendente')
|
|
157
|
+
.option('--no-remote-check', 'Não consulta o remoto pelos documentos ausentes (offline)')
|
|
158
|
+
.option('--only <passo>', 'No modo --pr: roda só code-review ou só qa')
|
|
159
|
+
.option('--json', 'Imprime a decisão em JSON')
|
|
160
|
+
.action(async (issue, options) => {
|
|
161
|
+
const { run } = await import('./commands/run.mjs');
|
|
162
|
+
await run(issue, options).catch(err => { console.error(err.message); process.exit(1); });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command('mode')
|
|
167
|
+
.description('Mostra ou alterna o modo de execução: `actions` (workflows) ou `local` (esta máquina)')
|
|
168
|
+
.argument('[modo]', 'actions | local (sem argumento: só mostra o estado)')
|
|
169
|
+
.option('--dry-run', 'Mostra o que mudaria sem alterar nada')
|
|
170
|
+
.action(async (target, options) => {
|
|
171
|
+
const { mode } = await import('./commands/mode.mjs');
|
|
172
|
+
const result = await mode({ target, ...options })
|
|
173
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
174
|
+
// Escrever só o config é meio-interruptor: a CLI passa a agir como local
|
|
175
|
+
// e os workflows continuam disparando. O comando já avisa em vermelho,
|
|
176
|
+
// mas sair 0 fazia qualquer script (ou agente) que o encadeasse ler
|
|
177
|
+
// sucesso — e seguir em frente achando que o CI estava desligado.
|
|
178
|
+
if (result?.variableApplied === false) process.exit(1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
program
|
|
182
|
+
.command('update')
|
|
183
|
+
.description('Detecta o que está desatualizado (skill, .spec-wave.json, workflows/labels do repo) e atualiza só o que mudou')
|
|
184
|
+
.option('--global', 'Verifica a skill no escopo do usuário (padrão: projeto)')
|
|
185
|
+
.option('--skip-skill', 'Não verifica/atualiza a skill instalada')
|
|
186
|
+
.option('--skip-config', 'Não verifica/atualiza o .spec-wave.json local')
|
|
187
|
+
.option('--skip-repo', 'Não verifica/atualiza workflows e labels do repo')
|
|
188
|
+
.option('--branch [nome]', 'Envia os arquivos do repo como Pull Request numa branch, em um único commit (sem valor: spec-wave/update-v<versão>)')
|
|
189
|
+
.option('--config-in-pr', 'Força incluir o .spec-wave.json no Pull Request')
|
|
190
|
+
.option('--no-config-in-pr', 'Força manter o .spec-wave.json fora do Pull Request')
|
|
191
|
+
.option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
|
|
192
|
+
.option('--yes', 'Aplica sem pedir confirmação')
|
|
193
|
+
.action(async (options) => {
|
|
194
|
+
const { update } = await import('./commands/update.mjs');
|
|
195
|
+
await update(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
program
|
|
199
|
+
.command('install-skill')
|
|
200
|
+
.description('Instala a skill spec-wave no(s) agente(s) detectado(s): Claude Code, Codex, Cursor, opencode, Cline, Kilo, Antigravity, AGENTS.md')
|
|
201
|
+
.option('--agent <names>', 'Agente(s) alvo, separados por vírgula (pula a detecção)')
|
|
202
|
+
.option('--all', 'Instala em todos os agentes detectados')
|
|
203
|
+
.option('--global', 'Instala no escopo do usuário (padrão: projeto)')
|
|
204
|
+
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
205
|
+
.option('--force', 'Sobrescreve arquivos existentes sem confirmar')
|
|
206
|
+
.option('--yes', 'Modo não-interativo')
|
|
207
|
+
.action(async (options) => {
|
|
208
|
+
const { installSkill } = await import('./commands/install-skill.mjs');
|
|
209
|
+
await installSkill(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command('uninstall')
|
|
214
|
+
.description('Remove labels, arquivos .github e o .spec-wave.json (mantém o GitHub Project)')
|
|
215
|
+
.option('--repo <owner/repo>', 'Repositório (padrão: lê do .spec-wave.json)')
|
|
216
|
+
.option('--skip-labels', 'Não remove as labels')
|
|
217
|
+
.option('--skip-files', 'Não remove os arquivos .github')
|
|
218
|
+
.option('--keep-config', 'Mantém o .spec-wave.json local')
|
|
219
|
+
.option('--dry-run', 'Mostra o que seria removido sem alterar nada')
|
|
220
|
+
.option('--yes', 'Não pede confirmação')
|
|
221
|
+
.action(async (options) => {
|
|
222
|
+
const { uninstall } = await import('./commands/uninstall.mjs');
|
|
223
|
+
await uninstall(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
program
|
|
227
|
+
.command('generate-plan')
|
|
228
|
+
.description('Gera plan.md para uma Feature — roda no GitHub Action ou localmente')
|
|
229
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
230
|
+
.action(async (options) => {
|
|
231
|
+
const { generatePlan } = await import('./commands/generate-plan.mjs');
|
|
232
|
+
await generatePlan(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
program
|
|
236
|
+
.command('critique')
|
|
237
|
+
.description('Critica um documento COMO ESTÁ, sem regerar — para depois de corrigi-lo à mão')
|
|
238
|
+
.option('--issue-number <n>', 'Número da issue: critica o plan.md dela e comenta (fluxo canônico)')
|
|
239
|
+
.option('--file <caminho>', 'Critica ESTE arquivo e imprime o resultado — sem issue, sem label, sem contar tentativa')
|
|
240
|
+
.option('--kind <tipo>', 'plan | spec | stories | bug (default: inferido do nome do arquivo)')
|
|
241
|
+
.option('--fail-on-grave', 'Sai com código 1 se houver finding grave (útil em script)')
|
|
242
|
+
.action(async (options) => {
|
|
243
|
+
const { critique } = await import('./commands/generate-plan.mjs');
|
|
244
|
+
await critique(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
program
|
|
248
|
+
.command('generate-spec')
|
|
249
|
+
.description('Gera spec.md para uma Feature — roda no GitHub Action ou localmente')
|
|
250
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
const { generateSpec } = await import('./commands/generate-spec.mjs');
|
|
253
|
+
await generateSpec(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
program
|
|
257
|
+
.command('generate-bug')
|
|
258
|
+
.description('Gera bug.md para um Bug — roda no GitHub Action ou localmente')
|
|
259
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
260
|
+
.action(async (options) => {
|
|
261
|
+
const { generateBug } = await import('./commands/generate-bug.mjs');
|
|
262
|
+
await generateBug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
program
|
|
266
|
+
.command('validate')
|
|
267
|
+
.description('Valida os documentos de uma issue: spec.md+plan.md de Feature, bug.md de Bug (usado pelo GitHub Action)')
|
|
268
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
269
|
+
.action(async (options) => {
|
|
270
|
+
const { validate } = await import('./commands/validate.mjs');
|
|
271
|
+
// A reprova é um desfecho esperado do comando, não uma exceção — mas o exit
|
|
272
|
+
// code precisa continuar 1 para o job do Actions ficar vermelho.
|
|
273
|
+
const result = await validate(options)
|
|
274
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
275
|
+
if (result?.ok === false) process.exit(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
program
|
|
279
|
+
.command('decompose')
|
|
280
|
+
.description('Gera o rascunho da decomposição em decomposition.md; com --apply, cria as Stories/Tasks a partir do rascunho revisado — roda no Action ou localmente')
|
|
281
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
282
|
+
.option('--apply', 'Aplica o decomposition.md já revisado: cria as issues (sem esta flag, apenas gera/critica o rascunho)')
|
|
283
|
+
.action(async (options) => {
|
|
284
|
+
const { decompose } = await import('./commands/decompose.mjs');
|
|
285
|
+
await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
program
|
|
289
|
+
.command('code-review')
|
|
290
|
+
.description('Move Feature para Code Review ao abrir um PR (usado pelo GitHub Action)')
|
|
291
|
+
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
292
|
+
.option('--resolve-only', 'Só descobre a Feature-alvo e imprime feature=<n>, sem tocar no board')
|
|
293
|
+
.action(async (options) => {
|
|
294
|
+
const { codeReview } = await import('./commands/code-review.mjs');
|
|
295
|
+
await codeReview(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
program
|
|
299
|
+
.command('qa')
|
|
300
|
+
.description('Move Feature para QA ao aprovar um PR (usado pelo GitHub Action)')
|
|
301
|
+
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
302
|
+
.action(async (options) => {
|
|
303
|
+
const { qa } = await import('./commands/qa.mjs');
|
|
304
|
+
await qa(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
program
|
|
308
|
+
.command('implement')
|
|
309
|
+
.description('Aciona o spec-kit implement para uma Feature (Stories pendentes em ordem de dependência), uma Story (todas as tasks) ou uma Task')
|
|
310
|
+
.argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
|
|
311
|
+
.option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
|
|
312
|
+
.option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
|
|
313
|
+
.action(async (issue, options) => {
|
|
314
|
+
const { implement } = await import('./commands/implement.mjs');
|
|
315
|
+
await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
program
|
|
319
|
+
.command('order')
|
|
320
|
+
.description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
|
|
321
|
+
.argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
|
|
322
|
+
.action(async (feature) => {
|
|
323
|
+
const { order } = await import('./commands/order.mjs');
|
|
324
|
+
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
program
|
|
328
|
+
.command('task')
|
|
329
|
+
.description('Gerencia uma Task no board: start (Status "In Progress") ou done (Done)')
|
|
330
|
+
.argument('<action>', 'Ação: start ou done')
|
|
331
|
+
.argument('<n>', 'Número da issue da Task, ex.: 12 ou #12')
|
|
332
|
+
.action(async (action, n) => {
|
|
333
|
+
const { task } = await import('./commands/task.mjs');
|
|
334
|
+
await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
program
|
|
338
|
+
.command('move')
|
|
339
|
+
.description('Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede')
|
|
340
|
+
.argument('<n>', 'Número da issue, ex.: 8 ou #8')
|
|
341
|
+
.argument('<etapa>', 'Etapa de destino, com ou sem emoji, ex.: "code review", "Homologação", "🎉 Done"')
|
|
342
|
+
.option('--status <valor>', 'Valor do campo Status no destino: Todo, In Progress ou Done (default: Todo)')
|
|
343
|
+
.action(async (n, etapa, options) => {
|
|
344
|
+
const { move } = await import('./commands/move.mjs');
|
|
345
|
+
await move({ issue: n, stage: etapa, ...options })
|
|
346
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
program
|
|
350
|
+
.command('repair-stage')
|
|
351
|
+
.description('Corrige a Etapa de itens que a automação errou — inclusive retrocedendo. Exige --yes e --reason, e registra o reparo na issue')
|
|
352
|
+
.argument('<issues>', 'Número(s) da(s) issue(s), ex.: 529 ou 529,530,531')
|
|
353
|
+
.argument('<etapa>', 'Etapa correta, com ou sem emoji, ex.: "ready", "✅ Ready"')
|
|
354
|
+
.option('--reason <motivo>', 'Por que o reparo é necessário (vai para o comentário de auditoria)')
|
|
355
|
+
.option('--status <valor>', 'Também corrige o Status: Todo, In Progress ou Done (default: não mexe)')
|
|
356
|
+
.option('--yes', 'Confirma o reparo (obrigatório)')
|
|
357
|
+
.option('--dry-run', 'Mostra o que seria reparado sem alterar nada')
|
|
358
|
+
.action(async (issues, etapa, options) => {
|
|
359
|
+
const { repairStage } = await import('./commands/repair-stage.mjs');
|
|
360
|
+
await repairStage(issues, etapa, options)
|
|
361
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
program
|
|
365
|
+
.command('story')
|
|
366
|
+
.description('Gerencia uma Story no board: review (move para Code Review)')
|
|
367
|
+
.argument('<action>', 'Ação: review')
|
|
368
|
+
.argument('<n>', 'Número da issue da Story, ex.: 12 ou #12')
|
|
369
|
+
.action(async (action, n) => {
|
|
370
|
+
const { story } = await import('./commands/story.mjs');
|
|
371
|
+
await story({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
program
|
|
375
|
+
.command('dev-agent')
|
|
376
|
+
.description('Instala (--install/--build) ou executa (--run) o spec-wave-agent nesta máquina')
|
|
377
|
+
.option('--install', 'Baixa o binário da release, gera a config e (com --service) o serviço')
|
|
378
|
+
.option('--build', 'Clona o repo do agente e compila com cargo (alternativa ao --install)')
|
|
379
|
+
.option('--run', 'Executa o agente em foreground (Ctrl+C encerra com checkpoint)')
|
|
380
|
+
.option('--service', 'No --install/--build: também instala e habilita systemd/launchd')
|
|
381
|
+
.option('--tag <tag>', 'Release (--install) ou branch/tag (--build); padrão: última release / main')
|
|
382
|
+
.option('--debug', 'No --run: RUST_LOG=debug')
|
|
383
|
+
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
384
|
+
.option('--force', 'Reinstala o binário e regrava a config')
|
|
385
|
+
.option('--yes', 'Modo não-interativo')
|
|
386
|
+
.action(async (options) => {
|
|
387
|
+
const { devAgent } = await import('./commands/dev-agent.mjs');
|
|
388
|
+
await devAgent(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
program
|
|
392
|
+
.command('doctor')
|
|
393
|
+
.description('Diagnostica a configuração do spec-wave no repositório atual')
|
|
394
|
+
.action(async () => {
|
|
395
|
+
const { doctor } = await import('./commands/doctor.mjs');
|
|
396
|
+
await doctor().catch(err => { console.error(err.message); process.exit(1); });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return program;
|
|
400
|
+
}
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
34
34
|
import { formatDependencyLine, orderStories, renderOrderComment } from '../lib/dependencies.mjs';
|
|
35
35
|
import { lintLanguage } from '../lib/output-lint.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import { resolveDocDir } from '../lib/doc-paths.mjs';
|
|
37
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
37
38
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
38
39
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
39
40
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
@@ -156,16 +157,27 @@ const CHILD_PREFIX = { Feature: '[STORY]', RFC: '[TASK]' };
|
|
|
156
157
|
/**
|
|
157
158
|
* Modo do run (função PURA — testável).
|
|
158
159
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
160
|
+
* TRI-ESTADO, e a distinção é o que impede um estrago: `apply` pode ser `true`
|
|
161
|
+
* (aplique), `false` (NÃO aplique) ou `undefined` (ninguém decidiu — use a
|
|
162
|
+
* label). Só o `undefined` cai no fallback por label, que é o caminho do
|
|
163
|
+
* workflow (onde a label que disparou é a fonte de verdade) e do retry de
|
|
164
|
+
* runner.
|
|
165
|
+
*
|
|
166
|
+
* Antes, o default `apply = false` colapsava "não aplique" em "ninguém
|
|
167
|
+
* decidiu": o `run` despachava o rascunho com `apply: false` e a label
|
|
168
|
+
* `spec-wave:decompose-apply` esquecida na issue por uma Action que falhou
|
|
169
|
+
* promovia a execução a aplicação. O comando anunciava "gera/re-critica o
|
|
170
|
+
* rascunho" e CRIAVA Stories e Tasks — sem passar pelo portão de confirmação
|
|
171
|
+
* do `run`, que existe exatamente para isso.
|
|
161
172
|
*
|
|
162
173
|
* @param {object} params
|
|
163
174
|
* @param {Array<string|{name:string}>} [params.labels] labels da issue
|
|
164
|
-
* @param {boolean} [params.apply]
|
|
175
|
+
* @param {boolean} [params.apply] `true`/`false` decidem; `undefined` consulta a label
|
|
165
176
|
* @returns {'draft'|'apply'}
|
|
166
177
|
*/
|
|
167
|
-
export function resolveDecomposeMode({ labels = [], apply
|
|
168
|
-
if (apply) return 'apply';
|
|
178
|
+
export function resolveDecomposeMode({ labels = [], apply } = {}) {
|
|
179
|
+
if (apply === true) return 'apply';
|
|
180
|
+
if (apply === false) return 'draft';
|
|
169
181
|
return labelNames(labels).includes(LABEL_DECOMPOSE_APPLY) ? 'apply' : 'draft';
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -222,14 +234,6 @@ export function resolveInheritedMilestone(issue) {
|
|
|
222
234
|
return Number.isInteger(number) && number > 0 ? number : undefined;
|
|
223
235
|
}
|
|
224
236
|
|
|
225
|
-
// Diretório do documento por tipo. Feature usa o mesmo docs/features/<slug> da
|
|
226
|
-
// spec/plan; RFC ganha o seu, já que não passa por spec/plan.
|
|
227
|
-
function resolveDocDir(root, issue, type) {
|
|
228
|
-
const slug = slugify(issue.title);
|
|
229
|
-
const rel = type === 'RFC' ? `docs/rfcs/${slug}` : `docs/features/${slug}`;
|
|
230
|
-
return { slug, rel, dir: path.resolve(root || process.cwd(), rel) };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
237
|
// Lint de idioma sobre títulos+corpos gerados; retorna aviso pronto para
|
|
234
238
|
// anexar ao comentário final ('' se limpo).
|
|
235
239
|
function formatItemsLintWarning(texts) {
|
|
@@ -262,7 +266,7 @@ async function draftDecomposition(ctx) {
|
|
|
262
266
|
const { token, owner, repo, issue, issueNumber, type, labels, usage, root, runMode, docDir, docPath, docRel } = ctx;
|
|
263
267
|
const number = parseInt(issueNumber, 10);
|
|
264
268
|
const kind = DECOMPOSE_TARGETS[type]; // Feature → 'stories'; RFC → 'tasks'
|
|
265
|
-
const blobUrl =
|
|
269
|
+
const blobUrl = docBlobUrl({ owner, repo, pathRel: docRel, mode: runMode, root });
|
|
266
270
|
|
|
267
271
|
const specPath = path.join(docDir, 'spec.md');
|
|
268
272
|
const planPath = path.join(docDir, 'plan.md');
|
|
@@ -924,7 +928,10 @@ export function renderApplyFailureComment({ trigger, message, createdItems = [],
|
|
|
924
928
|
'Complete o que falta à mão, ou apague as issues acima antes de tentar de novo.';
|
|
925
929
|
}
|
|
926
930
|
|
|
927
|
-
|
|
931
|
+
// `apply` SEM default: `undefined` significa "ninguém decidiu" e é o que faz o
|
|
932
|
+
// fallback por label valer no workflow. Um default `false` aqui reintroduziria
|
|
933
|
+
// o bug, porque a CLI passa `undefined` quando a flag não é usada.
|
|
934
|
+
export async function decompose({ issueNumber, apply }) {
|
|
928
935
|
const token = await resolveToken();
|
|
929
936
|
// PROJECT_TOKEN deve ter scope "project" para atualizar GitHub Projects v2.
|
|
930
937
|
// Fallback para GITHUB_TOKEN (só funciona em repos pessoais sem org restrictions).
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
tokenMismatchWarning, parseActiveAccount,
|
|
14
14
|
} from '../api/auth.mjs';
|
|
15
15
|
import { getProjectSnapshot, listSubIssues } from '../api/github-graphql.mjs';
|
|
16
|
+
import { getRepoVariable } from '../api/github-rest.mjs';
|
|
16
17
|
import {
|
|
17
18
|
CONFIG_FILE, WORKFLOW_FILES, getProvider, DEFAULT_PROVIDER, AI_PROVIDERS, STATUS_OPTIONS,
|
|
18
19
|
RETIRED_STAGES, ALL_LABELS, allLabelsFor, LABEL_NEEDS_HUMAN, MODEL_LABEL_PREFIX,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
modelLabels,
|
|
21
22
|
} from '../config.mjs';
|
|
22
23
|
import { findConfigPath } from '../lib/project-root.mjs';
|
|
24
|
+
import { configuredMode, describeModeState, EXECUTION_VARIABLE } from '../lib/execution-mode.mjs';
|
|
25
|
+
import { unguardedWorkflows } from './mode.mjs';
|
|
23
26
|
import {
|
|
24
27
|
DEFAULT_MAX_TOKENS, supportsStrictSchema, resolveAiConfig,
|
|
25
28
|
} from '../lib/claude.mjs';
|
|
@@ -936,6 +939,34 @@ export function checkSpecKit(ctx) {
|
|
|
936
939
|
};
|
|
937
940
|
}
|
|
938
941
|
|
|
942
|
+
// Modo de execução: o config e a variável do repositório precisam concordar.
|
|
943
|
+
// Discordar não é detalhe — é o usuário achando que desligou os workflows e
|
|
944
|
+
// continuando a pagar minutos (ou o contrário: tudo pulado e nada rodando).
|
|
945
|
+
async function checkExecutionMode(ctx) {
|
|
946
|
+
const name = 'Modo de execução (Actions × local)';
|
|
947
|
+
const configured = configuredMode(ctx.cfg);
|
|
948
|
+
|
|
949
|
+
let variable; // undefined = não verificável
|
|
950
|
+
if (ctx.token && ctx.cfg?.owner && ctx.cfg?.repo) {
|
|
951
|
+
try {
|
|
952
|
+
variable = await getRepoVariable(ctx.token, ctx.cfg.owner, ctx.cfg.repo, EXECUTION_VARIABLE);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
variable = undefined;
|
|
955
|
+
if (err.status !== 403 && err.status !== 404) {
|
|
956
|
+
return { name, status: 'warn', detail: `Variável não verificável agora: ${err.message}` };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const estado = describeModeState({
|
|
962
|
+
configured,
|
|
963
|
+
variable,
|
|
964
|
+
unguardedWorkflows: unguardedWorkflows(ctx.root || ctx.cwd),
|
|
965
|
+
});
|
|
966
|
+
const status = estado.status === 'problem' ? 'fail' : estado.status;
|
|
967
|
+
return { name, status, detail: [estado.summary, ...estado.notes, ...estado.fixes].join('\n') };
|
|
968
|
+
}
|
|
969
|
+
|
|
939
970
|
async function checkWorkflows(ctx) {
|
|
940
971
|
const name = 'Workflows do Actions';
|
|
941
972
|
// Ancorado na raiz do projeto, não no cwd: rodar o doctor de um subdiretório
|
|
@@ -1037,6 +1068,7 @@ export async function doctor() {
|
|
|
1037
1068
|
checkDecompositions,
|
|
1038
1069
|
checkSpecKit,
|
|
1039
1070
|
checkWorkflows,
|
|
1071
|
+
checkExecutionMode,
|
|
1040
1072
|
];
|
|
1041
1073
|
const results = [];
|
|
1042
1074
|
const spinner = p.spinner();
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
import { generateDocument } from '../lib/claude.mjs';
|
|
16
16
|
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
17
17
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
18
|
-
import { loadConfig } from '../lib/project-root.mjs';
|
|
19
18
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
20
19
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
21
20
|
import { bugDocPaths } from '../lib/bug-doc.mjs';
|
|
22
|
-
import { commitGenerated,
|
|
21
|
+
import { commitGenerated, resolveFlowContext } from '../lib/flow-run.mjs';
|
|
22
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
23
23
|
import {
|
|
24
24
|
runCritique, resolveCritiqueAttempt, renderNeedsHumanComment,
|
|
25
25
|
} from '../lib/critique.mjs';
|
|
@@ -48,18 +48,12 @@ function isSpecWaveComment(body) {
|
|
|
48
48
|
|
|
49
49
|
export async function generateBug({ issueNumber }) {
|
|
50
50
|
const token = await resolveToken();
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Mesmo contexto dos outros geradores: GITHUB_REPOSITORY quando existe, o
|
|
52
|
+
// `.spec-wave.json` quando não. Este comando exigia a env crua e morria fora
|
|
53
|
+
// do runner mandando exportá-la — dentro de um repo que já sabe seu owner/repo.
|
|
54
|
+
const { owner, repo, root, config, mode } = resolveFlowContext({ command: 'generate-bug' });
|
|
53
55
|
const n = parseInt(issueNumber, 10);
|
|
54
56
|
|
|
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
57
|
console.log(`Buscando issue #${n}...`);
|
|
64
58
|
const issue = await getIssue(token, owner, repo, n);
|
|
65
59
|
|
|
@@ -126,7 +120,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
126
120
|
filePath: fileAbs,
|
|
127
121
|
content,
|
|
128
122
|
message: `docs: generate bug.md for ${slug} [spec-wave]`,
|
|
129
|
-
mode
|
|
123
|
+
mode,
|
|
130
124
|
});
|
|
131
125
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
132
126
|
|
|
@@ -142,7 +136,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
142
136
|
await commentOnIssue(
|
|
143
137
|
token, owner, repo, n,
|
|
144
138
|
'🐞 **bug.md gerado automaticamente!**\n\n' +
|
|
145
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
139
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root, config })})\n\n` +
|
|
146
140
|
'Revise a **causa raiz** e o **teste de regressão** — são as duas seções que decidem se ' +
|
|
147
141
|
'a correção ataca o defeito ou o sintoma. Quando estiver pronto, valide com:\n' +
|
|
148
142
|
`\`\`\`\ngh issue edit ${n} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
@@ -20,6 +20,7 @@ import { recordUsage } from '../lib/usage-report.mjs';
|
|
|
20
20
|
import { slugify } from '../lib/slugify.mjs';
|
|
21
21
|
import { resolveFromRoot } from '../lib/project-root.mjs';
|
|
22
22
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
23
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
23
24
|
import { buildTechContext } from '../lib/tech-context.mjs';
|
|
24
25
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
25
26
|
|
|
@@ -213,7 +214,7 @@ export async function generatePlan({ issueNumber }) {
|
|
|
213
214
|
await commentOnIssue(
|
|
214
215
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
215
216
|
`📋 **plan.md gerado automaticamente!**\n\n` +
|
|
216
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
217
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root, config })})\n\n` +
|
|
217
218
|
`Revise o plano e, quando estiver pronto, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
|
|
218
219
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
219
220
|
formatLintWarning(lintFindings)
|
|
@@ -7,6 +7,7 @@ import { recordUsage } from '../lib/usage-report.mjs';
|
|
|
7
7
|
import { slugify } from '../lib/slugify.mjs';
|
|
8
8
|
import { resolveFromRoot } from '../lib/project-root.mjs';
|
|
9
9
|
import { resolveFlowContext, commitGenerated } from '../lib/flow-run.mjs';
|
|
10
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
10
11
|
import { loadPrompt, systemPromptWithTools } from '../lib/prompt-loader.mjs';
|
|
11
12
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
12
13
|
import {
|
|
@@ -110,7 +111,7 @@ export async function generateSpec({ issueNumber }) {
|
|
|
110
111
|
await commentOnIssue(
|
|
111
112
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
112
113
|
`📋 **spec.md gerado automaticamente!**\n\n` +
|
|
113
|
-
`📄 Arquivo: [\`${fileRel}\`](
|
|
114
|
+
`📄 Arquivo: [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode, root })})\n\n` +
|
|
114
115
|
`Revise a especificação e, quando estiver pronto, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
|
|
115
116
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\`` +
|
|
116
117
|
formatLintWarning(lintFindings)
|