@spec-wave/cli 0.25.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/bin/spec-wave.mjs +4 -374
- package/package.json +1 -1
- package/src/cli.mjs +400 -0
- package/src/commands/decompose.mjs +20 -6
- package/src/commands/mode.mjs +16 -5
- package/src/commands/run.mjs +108 -13
- package/src/lib/execution-mode.mjs +22 -0
- package/src/lib/next-step.mjs +60 -4
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/run/SKILL.md +1 -1
package/bin/spec-wave.mjs
CHANGED
|
@@ -1,378 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
// Ponto de entrada: a montagem do programa vive em ../src/cli.mjs para que ela
|
|
4
|
+
// possa ser importada (doc gerada, testes de superfície) sem executar a CLI.
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
const pkg = JSON.parse(readFileSync(path.join(__dir, '..', 'package.json'), 'utf-8'));
|
|
6
|
+
import { buildProgram } from '../src/cli.mjs';
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
.name('spec-wave')
|
|
13
|
-
.description('Setup spec-driven GitHub workflow with Projects v2')
|
|
14
|
-
.version(pkg.version)
|
|
15
|
-
// Conta do `gh` a usar nesta execução. Existe porque um GH_TOKEN exportado no
|
|
16
|
-
// shell (um `.envrc` na raiz de um diretório de projetos, por exemplo) vale
|
|
17
|
-
// para repositórios de QUALQUER org abaixo dele — e o GraphQL do GitHub
|
|
18
|
-
// mascara o 403 resultante como "Could not resolve to a Repository".
|
|
19
|
-
.option('--account <login>', 'Conta do gh a usar (vence GITHUB_TOKEN/GH_TOKEN fora do CI)')
|
|
20
|
-
.hook('preAction', async (thisCommand) => {
|
|
21
|
-
const { account } = thisCommand.opts();
|
|
22
|
-
if (account) {
|
|
23
|
-
const { setAccountOverride } = await import('../src/api/auth.mjs');
|
|
24
|
-
setAccountOverride(account);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.command('init')
|
|
30
|
-
.description('Configura spec-wave em um repositório GitHub')
|
|
31
|
-
.option('--dry-run', 'Simula a configuração sem fazer alterações')
|
|
32
|
-
.option('--repo <owner/repo>', 'Repositório GitHub (ignora o wizard interativo)')
|
|
33
|
-
.option('--project-title <title>', 'Nome do GitHub Project (padrão: "<repo> — Spec Wave")')
|
|
34
|
-
.option('--skip-project', 'Pula a criação do GitHub Project (use se já foi criado)')
|
|
35
|
-
.option('--skip-labels', 'Pula a criação das labels')
|
|
36
|
-
.option('--skip-files', 'Pula a criação dos arquivos de workflow')
|
|
37
|
-
.option('--provider <provider>', 'Provider de IA dos workflows: anthropic ou openrouter')
|
|
38
|
-
.option('--model <model>', 'Modelo de IA usado pelos workflows (ex.: anthropic/claude-3.7-sonnet)')
|
|
39
|
-
.action(async (options) => {
|
|
40
|
-
const { init } = await import('../src/commands/init.mjs');
|
|
41
|
-
await init(options);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
program
|
|
45
|
-
.command('info')
|
|
46
|
-
.description('Mostra se o repositório atual foi inicializado e os dados do .spec-wave.json')
|
|
47
|
-
.option('--json', 'Saída em JSON (para uso programático)')
|
|
48
|
-
.action(async (options) => {
|
|
49
|
-
const { info } = await import('../src/commands/info.mjs');
|
|
50
|
-
await info(options);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
program
|
|
54
|
-
.command('refresh')
|
|
55
|
-
.description('Atualiza o .spec-wave.json local com os dados atuais do GitHub Project')
|
|
56
|
-
.option('--config', 'Re-consulta o Project e reescreve o .spec-wave.json')
|
|
57
|
-
.option('--stages', 'Acrescenta ao campo "Etapa" as colunas canônicas que faltam (nunca remove)')
|
|
58
|
-
.option('--dry-run', 'Com --stages: mostra o que seria enviado e não escreve nada')
|
|
59
|
-
.option('--yes', 'Com --stages: não pede confirmação')
|
|
60
|
-
.action(async (options) => {
|
|
61
|
-
const { refresh } = await import('../src/commands/refresh.mjs');
|
|
62
|
-
await refresh(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
program
|
|
66
|
-
.command('issue')
|
|
67
|
-
.description('Cria um work item (initiative/epic/feature/story/task...), opcionalmente como sub-issue, e adiciona ao board')
|
|
68
|
-
.requiredOption('--title <title>', 'Título (sem o prefixo de tipo, ex.: [FEATURE])')
|
|
69
|
-
.option('--type <type>', 'Tipo: initiative, epic, feature, story, task, bug, spike ou rfc', 'feature')
|
|
70
|
-
.option('--parent <n>', 'Número da issue pai (cria como sub-issue dela)')
|
|
71
|
-
.option('--body <text>', 'Descrição')
|
|
72
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3')
|
|
73
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
74
|
-
.action(async (options) => {
|
|
75
|
-
const { issue } = await import('../src/commands/issue.mjs');
|
|
76
|
-
await issue(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
program
|
|
80
|
-
.command('initiative')
|
|
81
|
-
.description('Atalho de `issue --type initiative` (nó raiz que agrupa Epics)')
|
|
82
|
-
.requiredOption('--title <title>', 'Título da initiative (sem o prefixo [INITIATIVE])')
|
|
83
|
-
.option('--body <text>', 'Descrição da initiative')
|
|
84
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
85
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
86
|
-
.action(async (options) => {
|
|
87
|
-
const { initiative } = await import('../src/commands/initiative.mjs');
|
|
88
|
-
await initiative(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
program
|
|
92
|
-
.command('feature')
|
|
93
|
-
.description('Atalho de `issue --type feature`')
|
|
94
|
-
.requiredOption('--title <title>', 'Título da feature (sem o prefixo [FEATURE])')
|
|
95
|
-
.option('--parent <n>', 'Número da Epic pai (cria como sub-issue dela)')
|
|
96
|
-
.option('--body <text>', 'Descrição da feature')
|
|
97
|
-
.option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
|
|
98
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
99
|
-
.action(async (options) => {
|
|
100
|
-
const { feature } = await import('../src/commands/feature.mjs');
|
|
101
|
-
await feature(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
program
|
|
105
|
-
.command('bug')
|
|
106
|
-
.description('Cria um Bug (atalho de `issue --type bug`) — nasce em 🐞 Triagem, ou ✅ Ready se P0')
|
|
107
|
-
.requiredOption('--title <title>', 'Título (sem o prefixo [BUG])')
|
|
108
|
-
.option('--parent <n>', 'Feature ou Story afetada (cria como sub-issue dela)')
|
|
109
|
-
.option('--body <text>', 'Descrição: passos, esperado e obtido')
|
|
110
|
-
.option('--priority <p>', 'Severidade: P0, P1, P2 ou P3')
|
|
111
|
-
.option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
|
|
112
|
-
.action(async (options) => {
|
|
113
|
-
const { bug } = await import('../src/commands/bug.mjs');
|
|
114
|
-
await bug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
program
|
|
118
|
-
.command('triage')
|
|
119
|
-
.description('Tria um Bug: accept (→ ✅ Ready), reject (fecha) ou duplicate (fecha)')
|
|
120
|
-
.argument('<action>', 'accept | reject | duplicate')
|
|
121
|
-
.argument('<issue>', 'Número da issue do Bug')
|
|
122
|
-
.option('--reason <texto>', 'Motivo da rejeição (obrigatório em reject)')
|
|
123
|
-
.option('--of <n>', 'Número da issue original (obrigatório em duplicate)')
|
|
124
|
-
.option('--severity <p>', 'Reclassifica a severidade ao aceitar: P0–P3')
|
|
125
|
-
.action(async (action, issue, options) => {
|
|
126
|
-
const { triage } = await import('../src/commands/triage.mjs');
|
|
127
|
-
await triage({ action, issue, ...options })
|
|
128
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
program
|
|
132
|
-
.command('run')
|
|
133
|
-
.description('Executa LOCALMENTE o próximo passo do fluxo (o que a label dispararia no Actions)')
|
|
134
|
-
.argument('[issue]', 'Número da issue (Feature, Bug ou RFC)')
|
|
135
|
-
.option('--pr <n>', 'Modo PR: decide entre code-review e qa pelo estado das reviews')
|
|
136
|
-
.option('--dry-run', 'Decide e explica sem executar nada')
|
|
137
|
-
.option('--yes', 'Confirma o passo que exige confirmação')
|
|
138
|
-
.option('--apply', 'Autoriza especificamente o decompose-apply (erra se o passo pendente for outro)')
|
|
139
|
-
.option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug')
|
|
140
|
-
.option('--max-steps <n>', 'Encadeia até N passos (padrão: 1)', '1')
|
|
141
|
-
.option('--force', 'Ignora o portão de label de gatilho pendente')
|
|
142
|
-
.option('--no-remote-check', 'Não consulta o remoto pelos documentos ausentes (offline)')
|
|
143
|
-
.option('--only <passo>', 'No modo --pr: roda só code-review ou só qa')
|
|
144
|
-
.option('--json', 'Imprime a decisão em JSON')
|
|
145
|
-
.action(async (issue, options) => {
|
|
146
|
-
const { run } = await import('../src/commands/run.mjs');
|
|
147
|
-
await run(issue, options).catch(err => { console.error(err.message); process.exit(1); });
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
program
|
|
151
|
-
.command('mode')
|
|
152
|
-
.description('Mostra ou alterna o modo de execução: `actions` (workflows) ou `local` (esta máquina)')
|
|
153
|
-
.argument('[modo]', 'actions | local (sem argumento: só mostra o estado)')
|
|
154
|
-
.option('--dry-run', 'Mostra o que mudaria sem alterar nada')
|
|
155
|
-
.action(async (target, options) => {
|
|
156
|
-
const { mode } = await import('../src/commands/mode.mjs');
|
|
157
|
-
await mode({ target, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
program
|
|
161
|
-
.command('update')
|
|
162
|
-
.description('Detecta o que está desatualizado (skill, .spec-wave.json, workflows/labels do repo) e atualiza só o que mudou')
|
|
163
|
-
.option('--global', 'Verifica a skill no escopo do usuário (padrão: projeto)')
|
|
164
|
-
.option('--skip-skill', 'Não verifica/atualiza a skill instalada')
|
|
165
|
-
.option('--skip-config', 'Não verifica/atualiza o .spec-wave.json local')
|
|
166
|
-
.option('--skip-repo', 'Não verifica/atualiza workflows e labels do repo')
|
|
167
|
-
.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>)')
|
|
168
|
-
.option('--config-in-pr', 'Força incluir o .spec-wave.json no Pull Request')
|
|
169
|
-
.option('--no-config-in-pr', 'Força manter o .spec-wave.json fora do Pull Request')
|
|
170
|
-
.option('--dry-run', 'Mostra o que seria atualizado sem alterar nada')
|
|
171
|
-
.option('--yes', 'Aplica sem pedir confirmação')
|
|
172
|
-
.action(async (options) => {
|
|
173
|
-
const { update } = await import('../src/commands/update.mjs');
|
|
174
|
-
await update(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
program
|
|
178
|
-
.command('install-skill')
|
|
179
|
-
.description('Instala a skill spec-wave no(s) agente(s) detectado(s): Claude Code, Codex, Cursor, opencode, Cline, Kilo, Antigravity, AGENTS.md')
|
|
180
|
-
.option('--agent <names>', 'Agente(s) alvo, separados por vírgula (pula a detecção)')
|
|
181
|
-
.option('--all', 'Instala em todos os agentes detectados')
|
|
182
|
-
.option('--global', 'Instala no escopo do usuário (padrão: projeto)')
|
|
183
|
-
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
184
|
-
.option('--force', 'Sobrescreve arquivos existentes sem confirmar')
|
|
185
|
-
.option('--yes', 'Modo não-interativo')
|
|
186
|
-
.action(async (options) => {
|
|
187
|
-
const { installSkill } = await import('../src/commands/install-skill.mjs');
|
|
188
|
-
await installSkill(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
program
|
|
192
|
-
.command('uninstall')
|
|
193
|
-
.description('Remove labels, arquivos .github e o .spec-wave.json (mantém o GitHub Project)')
|
|
194
|
-
.option('--repo <owner/repo>', 'Repositório (padrão: lê do .spec-wave.json)')
|
|
195
|
-
.option('--skip-labels', 'Não remove as labels')
|
|
196
|
-
.option('--skip-files', 'Não remove os arquivos .github')
|
|
197
|
-
.option('--keep-config', 'Mantém o .spec-wave.json local')
|
|
198
|
-
.option('--dry-run', 'Mostra o que seria removido sem alterar nada')
|
|
199
|
-
.option('--yes', 'Não pede confirmação')
|
|
200
|
-
.action(async (options) => {
|
|
201
|
-
const { uninstall } = await import('../src/commands/uninstall.mjs');
|
|
202
|
-
await uninstall(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
program
|
|
206
|
-
.command('generate-plan')
|
|
207
|
-
.description('Gera plan.md para uma Feature — roda no GitHub Action ou localmente')
|
|
208
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
209
|
-
.action(async (options) => {
|
|
210
|
-
const { generatePlan } = await import('../src/commands/generate-plan.mjs');
|
|
211
|
-
await generatePlan(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
program
|
|
215
|
-
.command('critique')
|
|
216
|
-
.description('Critica um documento COMO ESTÁ, sem regerar — para depois de corrigi-lo à mão')
|
|
217
|
-
.option('--issue-number <n>', 'Número da issue: critica o plan.md dela e comenta (fluxo canônico)')
|
|
218
|
-
.option('--file <caminho>', 'Critica ESTE arquivo e imprime o resultado — sem issue, sem label, sem contar tentativa')
|
|
219
|
-
.option('--kind <tipo>', 'plan | spec | stories | bug (default: inferido do nome do arquivo)')
|
|
220
|
-
.option('--fail-on-grave', 'Sai com código 1 se houver finding grave (útil em script)')
|
|
221
|
-
.action(async (options) => {
|
|
222
|
-
const { critique } = await import('../src/commands/generate-plan.mjs');
|
|
223
|
-
await critique(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
program
|
|
227
|
-
.command('generate-spec')
|
|
228
|
-
.description('Gera spec.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 { generateSpec } = await import('../src/commands/generate-spec.mjs');
|
|
232
|
-
await generateSpec(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
program
|
|
236
|
-
.command('generate-bug')
|
|
237
|
-
.description('Gera bug.md para um Bug — roda no GitHub Action ou localmente')
|
|
238
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
239
|
-
.action(async (options) => {
|
|
240
|
-
const { generateBug } = await import('../src/commands/generate-bug.mjs');
|
|
241
|
-
await generateBug(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
program
|
|
245
|
-
.command('validate')
|
|
246
|
-
.description('Valida os documentos de uma issue: spec.md+plan.md de Feature, bug.md de Bug (usado pelo GitHub Action)')
|
|
247
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
248
|
-
.action(async (options) => {
|
|
249
|
-
const { validate } = await import('../src/commands/validate.mjs');
|
|
250
|
-
// A reprova é um desfecho esperado do comando, não uma exceção — mas o exit
|
|
251
|
-
// code precisa continuar 1 para o job do Actions ficar vermelho.
|
|
252
|
-
const result = await validate(options)
|
|
253
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
254
|
-
if (result?.ok === false) process.exit(1);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
program
|
|
258
|
-
.command('decompose')
|
|
259
|
-
.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')
|
|
260
|
-
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
261
|
-
.option('--apply', 'Aplica o decomposition.md já revisado: cria as issues (sem esta flag, apenas gera/critica o rascunho)')
|
|
262
|
-
.action(async (options) => {
|
|
263
|
-
const { decompose } = await import('../src/commands/decompose.mjs');
|
|
264
|
-
await decompose(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
program
|
|
268
|
-
.command('code-review')
|
|
269
|
-
.description('Move Feature para Code Review ao abrir um PR (usado pelo GitHub Action)')
|
|
270
|
-
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
271
|
-
.option('--resolve-only', 'Só descobre a Feature-alvo e imprime feature=<n>, sem tocar no board')
|
|
272
|
-
.action(async (options) => {
|
|
273
|
-
const { codeReview } = await import('../src/commands/code-review.mjs');
|
|
274
|
-
await codeReview(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
program
|
|
278
|
-
.command('qa')
|
|
279
|
-
.description('Move Feature para QA ao aprovar um PR (usado pelo GitHub Action)')
|
|
280
|
-
.requiredOption('--pr-number <n>', 'Número do Pull Request')
|
|
281
|
-
.action(async (options) => {
|
|
282
|
-
const { qa } = await import('../src/commands/qa.mjs');
|
|
283
|
-
await qa(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
program
|
|
287
|
-
.command('implement')
|
|
288
|
-
.description('Aciona o spec-kit implement para uma Feature (Stories pendentes em ordem de dependência), uma Story (todas as tasks) ou uma Task')
|
|
289
|
-
.argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
|
|
290
|
-
.option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
|
|
291
|
-
.option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
|
|
292
|
-
.action(async (issue, options) => {
|
|
293
|
-
const { implement } = await import('../src/commands/implement.mjs');
|
|
294
|
-
await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
program
|
|
298
|
-
.command('order')
|
|
299
|
-
.description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
|
|
300
|
-
.argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
|
|
301
|
-
.action(async (feature) => {
|
|
302
|
-
const { order } = await import('../src/commands/order.mjs');
|
|
303
|
-
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
program
|
|
307
|
-
.command('task')
|
|
308
|
-
.description('Gerencia uma Task no board: start (Status "In Progress") ou done (Done)')
|
|
309
|
-
.argument('<action>', 'Ação: start ou done')
|
|
310
|
-
.argument('<n>', 'Número da issue da Task, ex.: 12 ou #12')
|
|
311
|
-
.action(async (action, n) => {
|
|
312
|
-
const { task } = await import('../src/commands/task.mjs');
|
|
313
|
-
await task({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
program
|
|
317
|
-
.command('move')
|
|
318
|
-
.description('Move qualquer item do board (Feature, Story, Task, Bug, RFC) para uma Etapa — a Etapa nunca retrocede')
|
|
319
|
-
.argument('<n>', 'Número da issue, ex.: 8 ou #8')
|
|
320
|
-
.argument('<etapa>', 'Etapa de destino, com ou sem emoji, ex.: "code review", "Homologação", "🎉 Done"')
|
|
321
|
-
.option('--status <valor>', 'Valor do campo Status no destino: Todo, In Progress ou Done (default: Todo)')
|
|
322
|
-
.action(async (n, etapa, options) => {
|
|
323
|
-
const { move } = await import('../src/commands/move.mjs');
|
|
324
|
-
await move({ issue: n, stage: etapa, ...options })
|
|
325
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
program
|
|
329
|
-
.command('repair-stage')
|
|
330
|
-
.description('Corrige a Etapa de itens que a automação errou — inclusive retrocedendo. Exige --yes e --reason, e registra o reparo na issue')
|
|
331
|
-
.argument('<issues>', 'Número(s) da(s) issue(s), ex.: 529 ou 529,530,531')
|
|
332
|
-
.argument('<etapa>', 'Etapa correta, com ou sem emoji, ex.: "ready", "✅ Ready"')
|
|
333
|
-
.option('--reason <motivo>', 'Por que o reparo é necessário (vai para o comentário de auditoria)')
|
|
334
|
-
.option('--status <valor>', 'Também corrige o Status: Todo, In Progress ou Done (default: não mexe)')
|
|
335
|
-
.option('--yes', 'Confirma o reparo (obrigatório)')
|
|
336
|
-
.option('--dry-run', 'Mostra o que seria reparado sem alterar nada')
|
|
337
|
-
.action(async (issues, etapa, options) => {
|
|
338
|
-
const { repairStage } = await import('../src/commands/repair-stage.mjs');
|
|
339
|
-
await repairStage(issues, etapa, options)
|
|
340
|
-
.catch(err => { console.error(err.message); process.exit(1); });
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
program
|
|
344
|
-
.command('story')
|
|
345
|
-
.description('Gerencia uma Story no board: review (move para Code Review)')
|
|
346
|
-
.argument('<action>', 'Ação: review')
|
|
347
|
-
.argument('<n>', 'Número da issue da Story, ex.: 12 ou #12')
|
|
348
|
-
.action(async (action, n) => {
|
|
349
|
-
const { story } = await import('../src/commands/story.mjs');
|
|
350
|
-
await story({ action, issue: n }).catch(err => { console.error(err.message); process.exit(1); });
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
program
|
|
354
|
-
.command('dev-agent')
|
|
355
|
-
.description('Instala (--install/--build) ou executa (--run) o spec-wave-agent nesta máquina')
|
|
356
|
-
.option('--install', 'Baixa o binário da release, gera a config e (com --service) o serviço')
|
|
357
|
-
.option('--build', 'Clona o repo do agente e compila com cargo (alternativa ao --install)')
|
|
358
|
-
.option('--run', 'Executa o agente em foreground (Ctrl+C encerra com checkpoint)')
|
|
359
|
-
.option('--service', 'No --install/--build: também instala e habilita systemd/launchd')
|
|
360
|
-
.option('--tag <tag>', 'Release (--install) ou branch/tag (--build); padrão: última release / main')
|
|
361
|
-
.option('--debug', 'No --run: RUST_LOG=debug')
|
|
362
|
-
.option('--dry-run', 'Mostra o que seria instalado sem gravar')
|
|
363
|
-
.option('--force', 'Reinstala o binário e regrava a config')
|
|
364
|
-
.option('--yes', 'Modo não-interativo')
|
|
365
|
-
.action(async (options) => {
|
|
366
|
-
const { devAgent } = await import('../src/commands/dev-agent.mjs');
|
|
367
|
-
await devAgent(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
program
|
|
371
|
-
.command('doctor')
|
|
372
|
-
.description('Diagnostica a configuração do spec-wave no repositório atual')
|
|
373
|
-
.action(async () => {
|
|
374
|
-
const { doctor } = await import('../src/commands/doctor.mjs');
|
|
375
|
-
await doctor().catch(err => { console.error(err.message); process.exit(1); });
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
program.parse();
|
|
8
|
+
buildProgram().parse();
|
package/package.json
CHANGED
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
|
+
}
|
|
@@ -157,16 +157,27 @@ const CHILD_PREFIX = { Feature: '[STORY]', RFC: '[TASK]' };
|
|
|
157
157
|
/**
|
|
158
158
|
* Modo do run (função PURA — testável).
|
|
159
159
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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.
|
|
162
172
|
*
|
|
163
173
|
* @param {object} params
|
|
164
174
|
* @param {Array<string|{name:string}>} [params.labels] labels da issue
|
|
165
|
-
* @param {boolean} [params.apply]
|
|
175
|
+
* @param {boolean} [params.apply] `true`/`false` decidem; `undefined` consulta a label
|
|
166
176
|
* @returns {'draft'|'apply'}
|
|
167
177
|
*/
|
|
168
|
-
export function resolveDecomposeMode({ labels = [], apply
|
|
169
|
-
if (apply) return 'apply';
|
|
178
|
+
export function resolveDecomposeMode({ labels = [], apply } = {}) {
|
|
179
|
+
if (apply === true) return 'apply';
|
|
180
|
+
if (apply === false) return 'draft';
|
|
170
181
|
return labelNames(labels).includes(LABEL_DECOMPOSE_APPLY) ? 'apply' : 'draft';
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -917,7 +928,10 @@ export function renderApplyFailureComment({ trigger, message, createdItems = [],
|
|
|
917
928
|
'Complete o que falta à mão, ou apague as issues acima antes de tentar de novo.';
|
|
918
929
|
}
|
|
919
930
|
|
|
920
|
-
|
|
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 }) {
|
|
921
935
|
const token = await resolveToken();
|
|
922
936
|
// PROJECT_TOKEN deve ter scope "project" para atualizar GitHub Projects v2.
|
|
923
937
|
// Fallback para GITHUB_TOKEN (só funciona em repos pessoais sem org restrictions).
|
package/src/commands/mode.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { CONFIG_FILE, WORKFLOW_FILES } from '../config.mjs';
|
|
|
21
21
|
import { updateConfig } from '../lib/config-file.mjs';
|
|
22
22
|
import {
|
|
23
23
|
EXECUTION_MODES, EXECUTION_VARIABLE, EXECUTION_GUARD,
|
|
24
|
-
configuredMode, variableValueFor, describeModeState,
|
|
24
|
+
configuredMode, variableValueFor, describeModeState, shouldWriteVariable,
|
|
25
25
|
} from '../lib/execution-mode.mjs';
|
|
26
26
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
27
27
|
|
|
@@ -100,20 +100,31 @@ export async function mode({ target, dryRun = false } = {}) {
|
|
|
100
100
|
|
|
101
101
|
const esperado = variableValueFor(alvo);
|
|
102
102
|
const mudaConfig = atual !== alvo;
|
|
103
|
-
|
|
103
|
+
// `undefined` é "não deu para LER" (403 — a variável exige admin), e não
|
|
104
|
+
// "está como deveria". Tratar os dois iguais fazia o comando pular a escrita
|
|
105
|
+
// e ainda assim anunciar que config e variável coincidiam: o usuário saía
|
|
106
|
+
// achando que desligou o CI, com os workflows armados e o minuto sendo
|
|
107
|
+
// cobrado — exatamente o meio-caminho que este comando existe para evitar.
|
|
108
|
+
//
|
|
109
|
+
// Não conseguir ler quase sempre significa não conseguir escrever. Tentar e
|
|
110
|
+
// falhar com a mensagem certa é honesto; não tentar e dizer "coincidem" não.
|
|
111
|
+
const mudaVariavel = shouldWriteVariable({ variable: variavel, expected: esperado });
|
|
104
112
|
|
|
105
113
|
if (!mudaConfig && !mudaVariavel) {
|
|
106
114
|
p.log.success(`Já está em ${chalk.bold(alvo)} — config e variável do repositório coincidem.`);
|
|
107
115
|
p.outro('Nada a fazer.');
|
|
108
|
-
return { mode: alvo, variable: variavel, changed: false };
|
|
116
|
+
return { mode: alvo, variable: variavel, changed: false, variableApplied: true };
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
if (dryRun) {
|
|
112
120
|
if (mudaConfig) p.log.info(`${CONFIG_FILE}: execution.mode ${atual} → ${alvo}`);
|
|
113
121
|
if (mudaVariavel) {
|
|
122
|
+
const atualDaVariavel = variavel === undefined
|
|
123
|
+
? 'valor atual desconhecido — sem permissão para ler'
|
|
124
|
+
: `valor atual: ${variavel === null ? 'ausente' : variavel}`;
|
|
114
125
|
p.log.info(esperado === null
|
|
115
|
-
? `Variável ${EXECUTION_VARIABLE}: remover (
|
|
116
|
-
: `Variável ${EXECUTION_VARIABLE}: definir como "${esperado}"`);
|
|
126
|
+
? `Variável ${EXECUTION_VARIABLE}: remover (${atualDaVariavel})`
|
|
127
|
+
: `Variável ${EXECUTION_VARIABLE}: definir como "${esperado}" (${atualDaVariavel})`);
|
|
117
128
|
}
|
|
118
129
|
p.outro('Dry-run: nada foi alterado.');
|
|
119
130
|
return { mode: atual, variable: variavel, changed: false };
|
package/src/commands/run.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { existsOnRemote } from '../lib/doc-availability.mjs';
|
|
|
26
26
|
import { featureDocPaths, bugDocPaths } from '../lib/doc-paths.mjs';
|
|
27
27
|
import { isActionsRun, resolveFlowContext } from '../lib/flow-run.mjs';
|
|
28
28
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
29
|
-
import { nextStep, resolveForcedStep, STEPS } from '../lib/next-step.mjs';
|
|
29
|
+
import { nextStep, resolveForcedStep, stepDocs, docsForType, STEPS } from '../lib/next-step.mjs';
|
|
30
30
|
import { nextPrStep, reviewVerdict } from '../lib/pr-step.mjs';
|
|
31
31
|
import { configuredMode } from '../lib/execution-mode.mjs';
|
|
32
32
|
import { labelNames } from '../config.mjs';
|
|
@@ -41,8 +41,44 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
|
41
41
|
// comentário de uso. Fica em .git/ porque já é ignorado e é por clone.
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Diretório .git COMPARTILHADO do clone (função com I/O, isolada para teste).
|
|
46
|
+
*
|
|
47
|
+
* Montar `<root>/.git` à mão assume que `.git` é um diretório — e num git
|
|
48
|
+
* worktree ele é um ARQUIVO com `gitdir: <caminho real>`. O `mkdirSync` do
|
|
49
|
+
* acquireLock estourava ENOTDIR ali, derrubando TODO `spec-wave run` de dentro
|
|
50
|
+
* de um worktree, inclusive `--dry-run`, antes de qualquer trabalho.
|
|
51
|
+
*
|
|
52
|
+
* `--git-common-dir` e não `--git-dir`: num worktree o `--git-dir` é
|
|
53
|
+
* `.git/worktrees/<nome>`, o que tornaria o lock por WORKTREE. Dois `run` na
|
|
54
|
+
* mesma issue em worktrees diferentes rodariam em paralelo commitando na mesma
|
|
55
|
+
* branch — exatamente o que este lock existe para impedir. O comum preserva o
|
|
56
|
+
* "é por clone" que o comentário acima declara.
|
|
57
|
+
*
|
|
58
|
+
* O caminho devolvido é RELATIVO ao cwd quando se está no clone principal
|
|
59
|
+
* (`.git` na raiz, `../../.git` num subdiretório) e absoluto de dentro de um
|
|
60
|
+
* worktree — daí o `path.resolve`. Sem ele, rodar de um subdiretório criaria um
|
|
61
|
+
* `.git/` novo ali dentro, que é um estrago pior e mais silencioso que o ENOTDIR.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} [root] raiz do projeto (a do .spec-wave.json)
|
|
64
|
+
* @returns {string} caminho absoluto do diretório .git compartilhado
|
|
65
|
+
*/
|
|
66
|
+
export function gitCommonDir(root) {
|
|
67
|
+
const cwd = root || process.cwd();
|
|
68
|
+
try {
|
|
69
|
+
const out = execSync('git rev-parse --git-common-dir', {
|
|
70
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
}).trim();
|
|
72
|
+
if (out) return path.resolve(cwd, out);
|
|
73
|
+
} catch {
|
|
74
|
+
// Fora de um repositório git: cai no palpite antigo. O `run` vai falhar
|
|
75
|
+
// adiante de qualquer forma (ele commita e faz push), e com mensagem melhor.
|
|
76
|
+
}
|
|
77
|
+
return path.join(cwd, '.git');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function lockPath(root, key) {
|
|
81
|
+
return path.join(gitCommonDir(root), 'spec-wave', `run-${key}.lock`);
|
|
46
82
|
}
|
|
47
83
|
|
|
48
84
|
function acquireLock(root, key) {
|
|
@@ -126,12 +162,34 @@ function localDocStates(issue, type, root) {
|
|
|
126
162
|
* clone. É a diferença entre "ainda não foi gerado" e "foi gerado e você não
|
|
127
163
|
* puxou" — a segunda leva a sobrescrever trabalho publicado.
|
|
128
164
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Documentos que vale sondar no remoto antes de acreditar na decisão (PURA).
|
|
167
|
+
*
|
|
168
|
+
* O G5 ("as labels afirmam um documento que não existe aqui") diz, com todas as
|
|
169
|
+
* letras, que o arquivo *não está no clone nem no remoto* — e manda renomear o
|
|
170
|
+
* diretório ou REGERAR. Enquanto a sonda só rodava para decisão não-bloqueada,
|
|
171
|
+
* essa frase era afirmada sem nunca ter perguntado ao remoto: num clone que
|
|
172
|
+
* está apenas atrás, o conselho levava a regerar por cima de um documento
|
|
173
|
+
* publicado, gastando uma chamada de IA para destruir o artefato bom. A
|
|
174
|
+
* correção certa era `git pull`.
|
|
175
|
+
*
|
|
176
|
+
* Nesse caso o passo é `none` e não há `writes`/`reads` de onde tirar a lista —
|
|
177
|
+
* sondamos os documentos do tipo.
|
|
178
|
+
*
|
|
179
|
+
* @param {{action: string, blocked: object|null}} decision
|
|
180
|
+
* @param {string|null} type
|
|
181
|
+
* @returns {string[]}
|
|
182
|
+
*/
|
|
183
|
+
export function docsToProbe(decision, type) {
|
|
184
|
+
if (decision.blocked?.code === 'inconsistent-state') return docsForType(type);
|
|
185
|
+
if (decision.blocked || !STEPS[decision.action]) return [];
|
|
186
|
+
// stepDocs e não `[writes, ...reads]`: `validate` e `decompose` leem
|
|
187
|
+
// documentos diferentes conforme o tipo, e sondar a lista errada devolve um
|
|
188
|
+
// `docs` que o G6 aprova por engano.
|
|
189
|
+
return stepDocs(decision.action, type);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function probeRemote({ alvos, docs, docPaths, token, owner, repo }) {
|
|
135
193
|
if (alvos.length === 0) return docs;
|
|
136
194
|
|
|
137
195
|
const atualizado = { ...docs };
|
|
@@ -180,6 +238,32 @@ function parityWarnings(issue, type) {
|
|
|
180
238
|
// Despacho
|
|
181
239
|
// ---------------------------------------------------------------------------
|
|
182
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Saída de `--json` (função PURA — devolve o texto, não imprime).
|
|
243
|
+
*
|
|
244
|
+
* UM documento, sempre. A impressão morava dentro do laço de passos, então
|
|
245
|
+
* `--max-steps N` emitia N documentos JSON concatenados — saída que nenhum
|
|
246
|
+
* parser aceita, numa flag que a skill anuncia justamente para ramificar
|
|
247
|
+
* programaticamente.
|
|
248
|
+
*
|
|
249
|
+
* O desfecho fica no TOPO, e não só dentro de `steps`: é o que quase todo
|
|
250
|
+
* consumidor lê, e é exatamente o formato que a execução de um passo só já
|
|
251
|
+
* produzia. Assim a correção não quebra quem já lia `.action`.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} params
|
|
254
|
+
* @param {string|number} params.issueNumber
|
|
255
|
+
* @param {string|null} params.type
|
|
256
|
+
* @param {object|null} params.ultimaDecisao
|
|
257
|
+
* @param {object[]} [params.passos]
|
|
258
|
+
* @returns {string} JSON indentado
|
|
259
|
+
*/
|
|
260
|
+
export function renderRunJson({ issueNumber, type, ultimaDecisao, passos = [] }) {
|
|
261
|
+
return JSON.stringify(
|
|
262
|
+
{ issue: Number(issueNumber), type: type ?? null, ...ultimaDecisao, steps: passos },
|
|
263
|
+
null, 2,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
183
267
|
async function dispatch(action, { issueNumber }) {
|
|
184
268
|
switch (action) {
|
|
185
269
|
case 'generate-spec': {
|
|
@@ -315,6 +399,9 @@ export async function run(issueArg, options = {}) {
|
|
|
315
399
|
const exitCodeAntes = process.exitCode;
|
|
316
400
|
try {
|
|
317
401
|
let ultimaDecisao = null;
|
|
402
|
+
// `--json` acumula e imprime UMA vez no fim (ver renderRunJson).
|
|
403
|
+
const passos = [];
|
|
404
|
+
let ultimoTipo = null;
|
|
318
405
|
|
|
319
406
|
for (let i = 0; i < teto; i++) {
|
|
320
407
|
const issue = await getIssue(token, owner, repo, parseInt(issueNumber, 10));
|
|
@@ -342,9 +429,12 @@ export async function run(issueArg, options = {}) {
|
|
|
342
429
|
};
|
|
343
430
|
|
|
344
431
|
let decision = nextStep({ ...entrada, docs });
|
|
345
|
-
if (remoteCheck
|
|
346
|
-
const
|
|
347
|
-
|
|
432
|
+
if (remoteCheck) {
|
|
433
|
+
const alvos = docsToProbe(decision, type).filter(doc => docs[doc] === 'missing');
|
|
434
|
+
if (alvos.length > 0) {
|
|
435
|
+
const docsRemotos = await probeRemote({ alvos, docs, docPaths, token, owner, repo });
|
|
436
|
+
decision = nextStep({ ...entrada, docs: docsRemotos });
|
|
437
|
+
}
|
|
348
438
|
}
|
|
349
439
|
ultimaDecisao = decision;
|
|
350
440
|
|
|
@@ -357,7 +447,8 @@ export async function run(issueArg, options = {}) {
|
|
|
357
447
|
);
|
|
358
448
|
}
|
|
359
449
|
|
|
360
|
-
|
|
450
|
+
ultimoTipo = type;
|
|
451
|
+
if (json) passos.push(decision);
|
|
361
452
|
else reportDecision(decision, { issueNumber, type, title: issue.title, warnings: parityWarnings(issue, type) });
|
|
362
453
|
|
|
363
454
|
if (decision.blocked) { process.exitCode = 2; break; }
|
|
@@ -389,6 +480,10 @@ export async function run(issueArg, options = {}) {
|
|
|
389
480
|
}
|
|
390
481
|
}
|
|
391
482
|
|
|
483
|
+
if (json) {
|
|
484
|
+
console.log(renderRunJson({ issueNumber, type: ultimoTipo, ultimaDecisao, passos }));
|
|
485
|
+
}
|
|
486
|
+
|
|
392
487
|
return ultimaDecisao;
|
|
393
488
|
} finally {
|
|
394
489
|
releaseLock(lock);
|
|
@@ -49,6 +49,28 @@ export function variableValueFor(mode) {
|
|
|
49
49
|
return mode === 'local' ? 'local' : null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A variável do repositório precisa ser escrita? (função PURA)
|
|
54
|
+
*
|
|
55
|
+
* Três estados entram, não dois: `string` (valor lido), `null` (lida, e não
|
|
56
|
+
* existe) e `undefined` (NÃO deu para ler — 403, a variável exige admin).
|
|
57
|
+
*
|
|
58
|
+
* `undefined` conta como "precisa escrever". Tratá-lo como "já está certo"
|
|
59
|
+
* fazia o comando pular a escrita e ainda anunciar que config e variável
|
|
60
|
+
* coincidiam — o usuário saía achando que desligou o CI, com os workflows
|
|
61
|
+
* armados. Não conseguir ler quase sempre significa não conseguir escrever;
|
|
62
|
+
* tentar e falhar com a mensagem certa é honesto, não tentar e declarar
|
|
63
|
+
* sucesso não é.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} params
|
|
66
|
+
* @param {string|null|undefined} params.variable valor remoto
|
|
67
|
+
* @param {string|null} params.expected valor que o modo-alvo exige (null = ausente)
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function shouldWriteVariable({ variable, expected }) {
|
|
71
|
+
return variable !== expected;
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
/**
|
|
53
75
|
* Diagnóstico do modo de execução (função PURA).
|
|
54
76
|
*
|
package/src/lib/next-step.mjs
CHANGED
|
@@ -44,11 +44,21 @@ export const STEPS = {
|
|
|
44
44
|
types: ['Feature'], creates: false, ai: true,
|
|
45
45
|
},
|
|
46
46
|
validate: {
|
|
47
|
+
// `reads` DEPENDE DO TIPO: a validação de Feature abre spec.md e plan.md, a
|
|
48
|
+
// de Bug abre bug.md. Declarar `[]` aqui desarmava o G6 justamente para o
|
|
49
|
+
// passo que só lê do disco — um clone atrasado reprovava uma Feature cujo
|
|
50
|
+
// plan.md existia no remoto, removia `spec-wave:ready` e comentava a falha
|
|
51
|
+
// na issue. Dano a estado COMPARTILHADO por uma condição puramente local.
|
|
47
52
|
trigger: LABEL_READY, cli: 'validate', writes: null, reads: [],
|
|
53
|
+
readsByType: { Feature: ['spec', 'plan'], Bug: ['bug'] },
|
|
48
54
|
types: ['Feature', 'Bug'], creates: false, ai: false,
|
|
49
55
|
},
|
|
50
56
|
decompose: {
|
|
57
|
+
// O rascunho de Feature é montado a partir de spec.md + plan.md; o de RFC
|
|
58
|
+
// não usa nenhum dos dois. Com `[]` e o plan só no remoto, o decompose
|
|
59
|
+
// gerava o rascunho com o plano VAZIO — sem erro, só pior.
|
|
51
60
|
trigger: LABEL_DECOMPOSE, cli: 'decompose', writes: 'decomposition', reads: [],
|
|
61
|
+
readsByType: { Feature: ['spec', 'plan'], RFC: [] },
|
|
52
62
|
types: ['Feature', 'RFC'], creates: false, ai: true,
|
|
53
63
|
},
|
|
54
64
|
'decompose-apply': {
|
|
@@ -71,13 +81,60 @@ export const STEPS = {
|
|
|
71
81
|
},
|
|
72
82
|
};
|
|
73
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Documentos que um passo LÊ, para um tipo de item (função PURA).
|
|
86
|
+
*
|
|
87
|
+
* Existe porque dois passos leem coisas diferentes conforme o tipo — `validate`
|
|
88
|
+
* abre spec+plan numa Feature e bug.md num Bug — e o G6 precisa da lista CERTA:
|
|
89
|
+
* ele é o que impede o passo de rodar contra um clone atrasado. Uma lista
|
|
90
|
+
* subdeclarada não causa erro visível, causa o passo rodando com o arquivo
|
|
91
|
+
* errado (ou ausente), que é o modo de falha caro.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} action nome da ação em STEPS
|
|
94
|
+
* @param {string|null} [type] tipo do work item
|
|
95
|
+
* @returns {string[]} documentos lidos
|
|
96
|
+
*/
|
|
97
|
+
export function stepReads(action, type) {
|
|
98
|
+
const step = STEPS[action];
|
|
99
|
+
if (!step) return [];
|
|
100
|
+
return step.readsByType?.[type] ?? step.reads;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Documentos que um passo TOCA (lê ou escreve), para um tipo (função PURA).
|
|
105
|
+
*
|
|
106
|
+
* É a lista que o G6, o aviso de "não deu para consultar o remoto" e a sonda do
|
|
107
|
+
* `run` precisam — os três erravam junto quando `reads` estava subdeclarado.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} action
|
|
110
|
+
* @param {string|null} [type]
|
|
111
|
+
* @returns {string[]}
|
|
112
|
+
*/
|
|
113
|
+
export function stepDocs(action, type) {
|
|
114
|
+
return [STEPS[action]?.writes, ...stepReads(action, type)].filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Documentos que um tipo usa (função PURA).
|
|
119
|
+
*
|
|
120
|
+
* O `run` precisa disto para sondar o remoto quando a decisão veio BLOQUEADA e
|
|
121
|
+
* o passo é `none` — aí não há `writes`/`reads` de onde tirar a lista, e é
|
|
122
|
+
* justamente o caso em que o G5 afirma "não está no clone nem no remoto".
|
|
123
|
+
*
|
|
124
|
+
* @param {string|null} [type]
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
export function docsForType(type) {
|
|
128
|
+
return DOCS_BY_TYPE[type] || [];
|
|
129
|
+
}
|
|
130
|
+
|
|
74
131
|
/** Tipos que o `run` sabe conduzir. Story/Task/Epic/Spike não têm passo de documento. */
|
|
75
132
|
export const RUNNABLE_TYPES = ['Feature', 'Bug', 'RFC'];
|
|
76
133
|
|
|
77
134
|
const TRIGGERS = Object.values(STEPS).map(s => s.trigger).filter(Boolean);
|
|
78
135
|
|
|
79
136
|
/** Documentos que cada tipo usa, na ordem em que o fluxo os produz. */
|
|
80
|
-
const DOCS_BY_TYPE = {
|
|
137
|
+
export const DOCS_BY_TYPE = {
|
|
81
138
|
Feature: ['spec', 'plan', 'decomposition'],
|
|
82
139
|
RFC: ['decomposition'],
|
|
83
140
|
Bug: ['bug'],
|
|
@@ -244,7 +301,7 @@ export function nextStep({
|
|
|
244
301
|
|
|
245
302
|
// G6 — documento que o passo lê ou escreve existe só no remoto. Gerar por cima
|
|
246
303
|
// levaria o `pull --rebase` do commit a brigar com o documento bom.
|
|
247
|
-
for (const doc of
|
|
304
|
+
for (const doc of stepDocs(action, type)) {
|
|
248
305
|
if (docState(docs, doc) === 'remote') {
|
|
249
306
|
return decided(action, `\`${pathOf(doc)}\` existe no repositório mas não no seu clone.`, params,
|
|
250
307
|
block('stale-checkout',
|
|
@@ -285,8 +342,7 @@ export function nextStep({
|
|
|
285
342
|
'Confirme com `--yes` se é isso mesmo.'));
|
|
286
343
|
}
|
|
287
344
|
|
|
288
|
-
const incertos =
|
|
289
|
-
.filter(Boolean)
|
|
345
|
+
const incertos = stepDocs(action, type)
|
|
290
346
|
.filter(doc => docState(docs, doc) === 'unknown');
|
|
291
347
|
const aviso = incertos.length > 0
|
|
292
348
|
? ` (não foi possível consultar o remoto por ${incertos.map(pathOf).join(', ')} — ` +
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"description": "Fluxo spec-driven no GitHub (RFC-001): Projects v2, labels de gatilho, spec/plan gerados por Action, decomposição em duas etapas e implementação orientada a Stories/Tasks.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Astratech",
|
|
@@ -26,7 +26,7 @@ npx @spec-wave/cli@latest mode actions # volta tudo para o CI
|
|
|
26
26
|
npx @spec-wave/cli@latest run <issue> --dry-run
|
|
27
27
|
```
|
|
28
28
|
2. Mostre ao usuário o passo, o motivo e o comando que rodaria. **Só então** execute sem a flag.
|
|
29
|
-
3. `--json` devolve a
|
|
29
|
+
3. `--json` devolve a decisão em JSON, quando você precisar ramificar programaticamente. É **um** documento: o desfecho no topo (`action`, `command`, `blocked`) e a sequência inteira em `steps` — útil com `--max-steps`.
|
|
30
30
|
|
|
31
31
|
## O que o `run` decide sozinho
|
|
32
32
|
|