@spec-wave/cli 0.6.0 → 0.7.1
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 +37 -0
- package/package.json +4 -1
- package/src/api/github-rest.mjs +60 -1
- package/src/commands/code-review.mjs +13 -52
- package/src/commands/decompose.mjs +288 -61
- package/src/commands/doctor.mjs +415 -0
- package/src/commands/generate-plan.mjs +95 -29
- package/src/commands/generate-spec.mjs +71 -28
- package/src/commands/implement.mjs +118 -3
- package/src/commands/init.mjs +2 -2
- package/src/commands/order.mjs +172 -0
- package/src/commands/qa.mjs +8 -46
- package/src/commands/story.mjs +128 -0
- package/src/commands/task.mjs +183 -0
- package/src/commands/validate.mjs +20 -3
- package/src/config.mjs +37 -0
- package/src/lib/board.mjs +106 -0
- package/src/lib/claude.mjs +138 -13
- package/src/lib/code-digest.mjs +183 -0
- package/src/lib/critique.mjs +160 -0
- package/src/lib/dependencies.mjs +92 -0
- package/src/lib/output-lint.mjs +92 -0
- package/src/lib/usage-report.mjs +167 -0
- package/src/setup/files.mjs +8 -20
- package/src/templates/skill/SKILL.md +104 -12
- package/src/templates/workflows/code-review.yml +4 -0
- package/src/templates/workflows/decompose.yml +7 -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 +4 -0
- package/src/templates/workflows/validate.yml +4 -0
- package/src/ui/wizard.mjs +2 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Telemetria de uso de IA (tokens/custo por chamada) → comentário único
|
|
2
|
+
// acumulado na issue da Feature.
|
|
3
|
+
//
|
|
4
|
+
// Cada invocação de generateDocument gera UMA entrada { at, action, provider,
|
|
5
|
+
// model, inputTokens, outputTokens, cost }. O recordUsage acumula as entradas
|
|
6
|
+
// num comentário markdown identificado pelo USAGE_MARKER: parse do comentário
|
|
7
|
+
// existente → concat com as novas → re-render → update (ou create).
|
|
8
|
+
//
|
|
9
|
+
// Contrato: registrar uso NUNCA derruba o fluxo principal — recordUsage é
|
|
10
|
+
// best-effort e engole qualquer erro com um console.warn.
|
|
11
|
+
|
|
12
|
+
import { listIssueComments, updateComment, commentOnIssue } from '../api/github-rest.mjs';
|
|
13
|
+
|
|
14
|
+
// Marcador HTML invisível que identifica o comentário de uso na issue.
|
|
15
|
+
export const USAGE_MARKER = '<!-- spec-wave:usage -->';
|
|
16
|
+
|
|
17
|
+
// Teto de linhas de dados na tabela — ao exceder, descarta as mais antigas
|
|
18
|
+
// (o comentário não pode crescer sem limite em Features longevas).
|
|
19
|
+
const MAX_ROWS = 100;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calcula o custo em USD a partir da tabela de preços (função PURA — testável).
|
|
23
|
+
*
|
|
24
|
+
* `pricing` vem do bloco `ai.pricing` do .spec-wave.json, no formato
|
|
25
|
+
* `{ [model]: { input, output } }` em USD por 1M de tokens. Sem pricing ou sem
|
|
26
|
+
* o modelo na tabela → null (custo desconhecido, nunca chuta).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} params
|
|
29
|
+
* @param {string} params.model modelo usado na chamada
|
|
30
|
+
* @param {number} params.inputTokens tokens de entrada
|
|
31
|
+
* @param {number} params.outputTokens tokens de saída
|
|
32
|
+
* @param {object|null} [params.pricing] tabela de preços por modelo
|
|
33
|
+
* @returns {number|null} custo em USD, ou null se desconhecido
|
|
34
|
+
*/
|
|
35
|
+
export function computeCost({ model, inputTokens, outputTokens, pricing } = {}) {
|
|
36
|
+
const price = pricing?.[model];
|
|
37
|
+
if (!price || typeof price.input !== 'number' || typeof price.output !== 'number') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return ((inputTokens || 0) * price.input + (outputTokens || 0) * price.output) / 1_000_000;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ISO timestamp → "YYYY-MM-DD HH:mm" em UTC (formato da coluna Data).
|
|
44
|
+
function formatAt(at) {
|
|
45
|
+
const date = new Date(at);
|
|
46
|
+
if (Number.isNaN(date.getTime())) return String(at || '');
|
|
47
|
+
return date.toISOString().slice(0, 16).replace('T', ' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Custo → "$X.XXXX"; null (desconhecido) → travessão.
|
|
51
|
+
function formatCost(cost) {
|
|
52
|
+
return typeof cost === 'number' ? `$${cost.toFixed(4)}` : '—';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Interpreta o comentário de uso de volta para entries (função PURA — round-trip
|
|
57
|
+
* do renderUsageComment). Linhas de header, separador e Total são ignoradas.
|
|
58
|
+
* `at` é reconstruído como `YYYY-MM-DDTHH:mm:00.000Z` (segundos zerados).
|
|
59
|
+
*
|
|
60
|
+
* @param {string} body corpo do comentário
|
|
61
|
+
* @returns {Array<object>|null} entries, ou null se sem marcador ou tabela irreconhecível
|
|
62
|
+
*/
|
|
63
|
+
export function parseUsageComment(body) {
|
|
64
|
+
const text = body || '';
|
|
65
|
+
if (!text.includes(USAGE_MARKER)) return null;
|
|
66
|
+
|
|
67
|
+
const entries = [];
|
|
68
|
+
let sawHeader = false;
|
|
69
|
+
for (const line of text.split('\n')) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed.startsWith('|')) continue;
|
|
72
|
+
const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
|
|
73
|
+
if (cells.length !== 6) continue;
|
|
74
|
+
if (cells[0] === 'Data (UTC)') { sawHeader = true; continue; } // header
|
|
75
|
+
if (/^:?-{3,}:?$/.test(cells[0])) continue; // separador
|
|
76
|
+
if (cells[0].includes('Total')) continue; // linha Total
|
|
77
|
+
|
|
78
|
+
const dateMatch = cells[0].match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/);
|
|
79
|
+
const inputTokens = Number.parseInt(cells[3], 10);
|
|
80
|
+
const outputTokens = Number.parseInt(cells[4], 10);
|
|
81
|
+
if (!dateMatch || Number.isNaN(inputTokens) || Number.isNaN(outputTokens)) continue;
|
|
82
|
+
|
|
83
|
+
const costMatch = cells[5].match(/^\$(\d+(?:\.\d+)?)$/);
|
|
84
|
+
entries.push({
|
|
85
|
+
at: `${dateMatch[1]}T${dateMatch[2]}:00.000Z`,
|
|
86
|
+
action: cells[1],
|
|
87
|
+
model: cells[2],
|
|
88
|
+
inputTokens,
|
|
89
|
+
outputTokens,
|
|
90
|
+
cost: costMatch ? Number.parseFloat(costMatch[1]) : null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Marcador presente mas tabela irreconhecível (sem header ou sem nenhuma
|
|
95
|
+
// linha de dados válida) → null: o chamador recomeça só com as novas entradas.
|
|
96
|
+
if (!sawHeader || entries.length === 0) return null;
|
|
97
|
+
return entries;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Renderiza as entries como comentário markdown (função PURA — testável).
|
|
102
|
+
*
|
|
103
|
+
* Cap de MAX_ROWS linhas de dados (descarta as mais antigas). A linha Total
|
|
104
|
+
* soma os tokens de todas as linhas exibidas e só os custos conhecidos —
|
|
105
|
+
* custos indisponíveis (—) ficam fora do total.
|
|
106
|
+
*
|
|
107
|
+
* @param {Array<object>} entries entradas de uso
|
|
108
|
+
* @returns {string} corpo markdown do comentário
|
|
109
|
+
*/
|
|
110
|
+
export function renderUsageComment(entries) {
|
|
111
|
+
const rows = (entries || []).slice(-MAX_ROWS);
|
|
112
|
+
|
|
113
|
+
const totalIn = rows.reduce((sum, e) => sum + (e.inputTokens || 0), 0);
|
|
114
|
+
const totalOut = rows.reduce((sum, e) => sum + (e.outputTokens || 0), 0);
|
|
115
|
+
const known = rows.filter(e => typeof e.cost === 'number');
|
|
116
|
+
const totalCost = known.length > 0
|
|
117
|
+
? formatCost(known.reduce((sum, e) => sum + e.cost, 0))
|
|
118
|
+
: '—';
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
USAGE_MARKER,
|
|
122
|
+
'📊 **Uso de IA (spec-wave)**',
|
|
123
|
+
'',
|
|
124
|
+
'| Data (UTC) | Ação | Modelo | Tokens in | Tokens out | Custo (USD) |',
|
|
125
|
+
'| --- | --- | --- | ---: | ---: | ---: |',
|
|
126
|
+
...rows.map(e =>
|
|
127
|
+
`| ${formatAt(e.at)} | ${e.action} | ${e.model} | ${e.inputTokens} | ${e.outputTokens} | ${formatCost(e.cost)} |`
|
|
128
|
+
),
|
|
129
|
+
`| **Total** | | | **${totalIn}** | **${totalOut}** | **${totalCost}** |`,
|
|
130
|
+
'',
|
|
131
|
+
'_Custos indisponíveis (—) não entram no total. Atualizado automaticamente pelo spec-wave._',
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Registra as entries no comentário de uso da issue (impura, best-effort).
|
|
137
|
+
*
|
|
138
|
+
* Acha o comentário com USAGE_MARKER, faz parse das entradas antigas, concatena
|
|
139
|
+
* as novas e atualiza (ou cria, se não existir). Qualquer erro vira só um
|
|
140
|
+
* console.warn — telemetria nunca derruba o fluxo principal.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} params
|
|
143
|
+
* @param {string} params.token token do GitHub
|
|
144
|
+
* @param {string} params.owner dono do repo
|
|
145
|
+
* @param {string} params.repo nome do repo
|
|
146
|
+
* @param {number} params.issueNumber número da issue
|
|
147
|
+
* @param {Array<object>} params.entries novas entradas de uso
|
|
148
|
+
*/
|
|
149
|
+
export async function recordUsage({ token, owner, repo, issueNumber, entries } = {}) {
|
|
150
|
+
if (!Array.isArray(entries) || entries.length === 0) return;
|
|
151
|
+
try {
|
|
152
|
+
const comments = await listIssueComments(token, owner, repo, issueNumber);
|
|
153
|
+
const existing = comments.find(c => (c.body || '').includes(USAGE_MARKER));
|
|
154
|
+
|
|
155
|
+
// Comentário antigo irreconhecível → recomeça só com as novas entradas.
|
|
156
|
+
const previous = existing ? parseUsageComment(existing.body) : null;
|
|
157
|
+
const body = renderUsageComment([...(previous || []), ...entries]);
|
|
158
|
+
|
|
159
|
+
if (existing?.id) {
|
|
160
|
+
await updateComment(token, owner, repo, existing.id, body);
|
|
161
|
+
} else {
|
|
162
|
+
await commentOnIssue(token, owner, repo, issueNumber, body);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.warn('Não foi possível registrar uso de IA:', err.message);
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/setup/files.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { upsertFile, getFileContent, isRepoInitialized } from '../api/github-rest.mjs';
|
|
5
|
+
import { WORKFLOW_FILES } from '../config.mjs';
|
|
5
6
|
|
|
6
7
|
const __dir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const TEMPLATES_DIR = path.join(__dir, '..', 'templates');
|
|
@@ -22,6 +23,8 @@ export async function setupFiles(token, owner, repo, spinner) {
|
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// Workflows vêm de WORKFLOW_FILES (config.mjs, fonte única) — adicionar um
|
|
27
|
+
// workflow novo lá basta para o init (e o update) instalarem.
|
|
25
28
|
const filesToCreate = [
|
|
26
29
|
{
|
|
27
30
|
path: '.github/ISSUE_TEMPLATE/plan-template.md',
|
|
@@ -33,26 +36,11 @@ export async function setupFiles(token, owner, repo, spinner) {
|
|
|
33
36
|
content: readTemplate('issue', 'spec-template.md'),
|
|
34
37
|
message: 'chore: add spec.md issue template [spec-wave]',
|
|
35
38
|
},
|
|
36
|
-
{
|
|
37
|
-
path:
|
|
38
|
-
content: readTemplate('workflows',
|
|
39
|
-
message:
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
path: '.github/workflows/generate-spec.yml',
|
|
43
|
-
content: readTemplate('workflows', 'generate-spec.yml'),
|
|
44
|
-
message: 'chore: add generate-spec workflow [spec-wave]',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
path: '.github/workflows/validate.yml',
|
|
48
|
-
content: readTemplate('workflows', 'validate.yml'),
|
|
49
|
-
message: 'chore: add validate workflow [spec-wave]',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
path: '.github/workflows/decompose.yml',
|
|
53
|
-
content: readTemplate('workflows', 'decompose.yml'),
|
|
54
|
-
message: 'chore: add decompose workflow [spec-wave]',
|
|
55
|
-
},
|
|
39
|
+
...WORKFLOW_FILES.map((file) => ({
|
|
40
|
+
path: `.github/workflows/${file}`,
|
|
41
|
+
content: readTemplate('workflows', file),
|
|
42
|
+
message: `chore: add ${file.replace(/\.yml$/, '')} workflow [spec-wave]`,
|
|
43
|
+
})),
|
|
56
44
|
];
|
|
57
45
|
|
|
58
46
|
for (let i = 0; i < filesToCreate.length; i++) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-wave
|
|
3
3
|
description: "Use when the user wants to set up a spec-driven GitHub workflow, create a Feature issue, generate spec.md or plan.md, decompose a Feature into Stories/Tasks, write RFC documentation, or audit and fix a Pull Request. Implements the RFC-001 workflow with GitHub Projects v2, labels, and AI-powered GitHub Actions."
|
|
4
|
-
argument-hint: "[info|setup|update|issue|feature|spec|plan|ready|decompose|implement|uninstall|rfc|fix-pr] [target]"
|
|
4
|
+
argument-hint: "[info|setup|update|doctor|issue|feature|spec|plan|ready|decompose|order|implement|task|story|uninstall|rfc|fix-pr] [target]"
|
|
5
5
|
user-invocable: true
|
|
6
6
|
allowed-tools:
|
|
7
7
|
- Bash(npx @spec-wave/cli *)
|
|
@@ -82,6 +82,10 @@ Labels de gatilho:
|
|
|
82
82
|
- `spec-wave:ready` → dispara `validate.yml` → valida ambos os arquivos
|
|
83
83
|
- `spec-wave:decompose` → dispara `decompose.yml` → gera Stories e Tasks
|
|
84
84
|
|
|
85
|
+
Labels de **estado** (gravadas pelas automações — **não** são gatilhos, não as adicione por conta própria):
|
|
86
|
+
- `spec-wave:critique-failed` → a crítica adversarial apontou contradições **graves** nos documentos; **bloqueia** o `spec-wave:ready` até ser removida (veja *Crítica adversarial* abaixo)
|
|
87
|
+
- `spec-wave:decomposed` → a Feature/RFC já foi decomposta; o `decompose` pula silenciosamente enquanto ela existir (veja *Guard de idempotência* abaixo)
|
|
88
|
+
|
|
85
89
|
A etapa **🚧 Desenvolvimento** é coberta pelo comando **local** `npx @spec-wave/cli implement <número>` (não é uma label/Action): lê uma Story ou Task e aciona o spec-kit para implementar. Veja `/spec-wave implement`.
|
|
86
90
|
|
|
87
91
|
---
|
|
@@ -158,6 +162,10 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
|
|
|
158
162
|
| `--issue-number <n>` | string (obrigatório) | Número da issue no GitHub. |
|
|
159
163
|
|
|
160
164
|
> ⚠️ Esses quatro comandos são executados pelos **GitHub Actions** (disparados por labels), **não** pela skill diretamente. Veja a *Regra fundamental*: para gerar plan/spec/decompor, adicione a **label** correspondente — não rode o comando à mão (a não ser para debug local).
|
|
165
|
+
>
|
|
166
|
+
> **Por tipo de issue:**
|
|
167
|
+
> - `generate-spec` / `generate-plan` → **apenas Features**. Para **Spike, RFC e Bug** a geração é **pulada** (o Action remove a label e comenta) — esses tipos não usam spec/plan.
|
|
168
|
+
> - `decompose` → **Feature** (gera Stories + Tasks) e **RFC** (gera **Tasks** diretamente, sem Stories). Para outros tipos, o Action recusa.
|
|
161
169
|
|
|
162
170
|
### `@spec-wave/cli implement` — aciona o spec-kit para uma Story ou Task (comando LOCAL)
|
|
163
171
|
| Flag/Arg | Tipo | Descrição |
|
|
@@ -166,7 +174,80 @@ Mesmas flags do `issue` (exceto `--type`, fixo em `feature`). Mantido para o flu
|
|
|
166
174
|
| `--feature-dir <path>` | string | Caminho `docs/features/<slug>` para anexar `spec.md`/`plan.md` como contexto (sobrescreve a resolução automática). |
|
|
167
175
|
| `--dry-run` | flag | Monta o contexto e imprime o comando do spec-kit **sem executar**. |
|
|
168
176
|
|
|
169
|
-
> Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Story** → coleta todas as Tasks (sub-issues) e aciona o spec-kit uma única vez; **Task** → só aquela task. Monta o contexto em `.spec-wave/implement-<n>.md` e chama o comando configurado em `specKit.command` (no `.spec-wave.json`) ou na env `SPEC_WAVE_IMPLEMENT_CMD`. Placeholders disponíveis no template: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`. Se nada estiver configurado, ele apenas monta o contexto e mostra como configurar (não executa). O contexto inclui instruções para o agente implementar as Tasks **sequencialmente, uma por vez** (nunca duas com Status "In Progress" ao mesmo tempo): cada Task usa o **Status** (In Progress) *dentro* da Etapa 🚧 Desenvolvimento e, **ao concluir, avança para a Etapa 🎉 Done com Status Done**. **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa da Story para 👀 Code Review** (Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** para Code Review quando **TODAS as suas Stories** já estiverem em Code Review — enquanto houver Story pendente, a Feature fica em 🚧 Desenvolvimento. Etapa só avança (nunca volta); Status mede o progresso dentro da etapa.
|
|
177
|
+
> Diferente dos quatro acima, `implement` roda **localmente** (lê `.spec-wave.json`, como `issue`), não por Action. Detecta o tipo da issue: **Story** → coleta todas as Tasks (sub-issues) e aciona o spec-kit uma única vez; **Task** → só aquela task. Monta o contexto em `.spec-wave/implement-<n>.md` e chama o comando configurado em `specKit.command` (no `.spec-wave.json`) ou na env `SPEC_WAVE_IMPLEMENT_CMD`. Placeholders disponíveis no template: `{tasksFile} {specFile} {planFile} {issue} {type} {title}`. Se nada estiver configurado, ele apenas monta o contexto e mostra como configurar (não executa). O contexto inclui os **comentários da issue**, um **digest do código recente** e um **aviso de dependências pendentes** quando a issue depende (linha `Depende de: #N` ou relação nativa *blocked by*) de outra que ainda não foi concluída — nesse caso, confirme com o usuário antes de seguir. Inclui também instruções para o agente implementar as Tasks **sequencialmente, uma por vez** (nunca duas com Status "In Progress" ao mesmo tempo): cada Task usa o **Status** (In Progress) *dentro* da Etapa 🚧 Desenvolvimento e, **ao concluir, avança para a Etapa 🎉 Done com Status Done**. **Ao concluir toda a Story**: fazer o commit, abrir o PR e **avançar a Etapa da Story para 👀 Code Review** (Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** para Code Review quando **TODAS as suas Stories** já estiverem em Code Review — enquanto houver Story pendente, a Feature fica em 🚧 Desenvolvimento. Etapa só avança (nunca volta); Status mede o progresso dentro da etapa.
|
|
178
|
+
|
|
179
|
+
### `@spec-wave/cli doctor` — preflight de auth e configuração (comando LOCAL)
|
|
180
|
+
Sem flags. Roda um checklist de diagnóstico no repositório atual: token GitHub (e a fonte dele), escopos (`repo`, `project`, `workflow` — com degradação para checks funcionais em fine-grained PATs), conta ativa do `gh` vs. owner, `.spec-wave.json` (campos e sincronia com o Project real), acesso ao repositório, configuração de IA (provider/modelo/`ai.models` + secrets do Actions) e presença dos workflows.
|
|
181
|
+
|
|
182
|
+
> Saída: `✓` ok, `✗` problema confirmado, `!` não verificável (best-effort — falha de rede nunca derruba o doctor). **Exit 1** se houver algum `✗`. **Quando rodar:** no início de uma sessão de trabalho, ou sempre que aparecer um erro estranho (ex.: **404 ao criar issues** — causa típica: token sem acesso ao repo/org, que o doctor aponta). É o primeiro passo de troubleshooting — prefira-o a depurar `gh api` na mão.
|
|
183
|
+
|
|
184
|
+
### `@spec-wave/cli order <feature>` — ordem de execução das Stories (comando LOCAL)
|
|
185
|
+
| Flag/Arg | Tipo | Descrição |
|
|
186
|
+
|----------|------|-----------|
|
|
187
|
+
| `<feature>` | string (obrigatório) | Número da issue da **Feature**, ex.: `12` ou `#12`. Argumento posicional. |
|
|
188
|
+
|
|
189
|
+
> Lista as Stories da Feature em **ordem topológica** pelas dependências (linha `Depende de: #N` no corpo + relação nativa *blocked by*, mescladas), com a Etapa atual de cada uma no board. Avisa sobre **ciclos de dependência** (essas Stories ficam fora da ordem — corrija as linhas `Depende de`) e sobre **dependências fora de ordem** (Story já em Desenvolvimento+ dependendo de outra que não está Done). Use antes de escolher qual Story implementar.
|
|
190
|
+
|
|
191
|
+
### `@spec-wave/cli task <start|done> <n>` — transições de Task no board (comando LOCAL)
|
|
192
|
+
| Flag/Arg | Tipo | Descrição |
|
|
193
|
+
|----------|------|-----------|
|
|
194
|
+
| `<action>` | string (obrigatório) | `start` (Etapa 🚧 Desenvolvimento + Status In Progress) ou `done` (Etapa 🎉 Done + Status Done). |
|
|
195
|
+
| `<n>` | string (obrigatório) | Número da issue da **Task**, ex.: `12` ou `#12`. |
|
|
196
|
+
|
|
197
|
+
> **Prefira este comando a mexer no board via GraphQL/`gh` manual** — ele embute as regras do fluxo: a **Etapa nunca retrocede** (se já estiver adiante, só o Status é ajustado) e **uma única Task "In Progress" por vez** dentro da mesma Story (`start` recusa, apontando a Task em andamento, se houver outra irmã em In Progress).
|
|
198
|
+
|
|
199
|
+
### `@spec-wave/cli story review <n>` — move a Story para Code Review (comando LOCAL)
|
|
200
|
+
| Flag/Arg | Tipo | Descrição |
|
|
201
|
+
|----------|------|-----------|
|
|
202
|
+
| `<action>` | string (obrigatório) | `review` (única ação hoje). |
|
|
203
|
+
| `<n>` | string (obrigatório) | Número da issue da **Story**, ex.: `12` ou `#12`. |
|
|
204
|
+
|
|
205
|
+
> Avança a Story para a Etapa **👀 Code Review** com Status **Todo** (o Status reinicia ao trocar de etapa). Mesma regra do `task`: a **Etapa nunca retrocede** — se a Story já estiver em Code Review ou adiante, o comando apenas informa a Etapa atual. Use no fim do `implement` de uma Story, em vez de mutações GraphQL manuais.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Crítica adversarial, idempotência e dependências (v0.7)
|
|
210
|
+
|
|
211
|
+
### Crítica adversarial (comentário 🔎)
|
|
212
|
+
|
|
213
|
+
Após o `generate-plan` e **antes** da criação de issues no `decompose`, um segundo agente de IA critica os documentos procurando contradições, lacunas e riscos. O resultado vira um comentário **🔎 Crítica adversarial (spec-wave)** na issue.
|
|
214
|
+
|
|
215
|
+
- Findings **graves** → o Action aplica a label **`spec-wave:critique-failed`**, que **bloqueia o `spec-wave:ready`** (o `validate` falha enquanto ela existir).
|
|
216
|
+
- **Fluxo de resolução:** (1) leia o comentário 🔎 na issue; (2) corrija `spec.md`/`plan.md` (regenere com as labels ou edite e commite); (3) remova a label: `gh issue edit <n> --remove-label "spec-wave:critique-failed"`; (4) re-aplique `spec-wave:ready` para validar de novo.
|
|
217
|
+
- Findings leves não bloqueiam — trate-os como revisão de qualidade.
|
|
218
|
+
|
|
219
|
+
### Guard de idempotência do decompose (`spec-wave:decomposed`)
|
|
220
|
+
|
|
221
|
+
O evento `labeled` pode redisparar (re-add da label, retry de runner). Para não duplicar Stories/Tasks, o `decompose` **pula** quando a issue já tem a label **`spec-wave:decomposed`** ou já tem sub-issues do tipo-alvo (`[STORY]` para Feature, `[TASK]` para RFC). Ao concluir com sucesso, o Action grava a label. Os workflows ainda usam `concurrency` por issue para serializar runs simultâneos.
|
|
222
|
+
|
|
223
|
+
**Para forçar um re-decompose:** remova a label (`gh issue edit <n> --remove-label "spec-wave:decomposed"`), **apague/feche as sub-issues antigas** (senão a detecção por sub-issues pula de novo) e re-adicione `spec-wave:decompose`.
|
|
224
|
+
|
|
225
|
+
### Dependências entre Stories (`Depende de: #N`)
|
|
226
|
+
|
|
227
|
+
O `decompose` grava nas Stories geradas uma linha **`Depende de: #N, #M`** no corpo e cria a relação nativa *blocked by* do GitHub. Essas dependências alimentam:
|
|
228
|
+
- `spec-wave order <feature>` → ordem topológica de execução;
|
|
229
|
+
- `spec-wave implement <n>` → **aviso** no contexto quando uma dependência ainda não está concluída (confirme com o usuário antes de implementar fora de ordem).
|
|
230
|
+
|
|
231
|
+
Não apague a linha `Depende de:` ao editar o corpo de uma Story; para mudar dependências, edite a linha (e/ou a relação *blocked by*).
|
|
232
|
+
|
|
233
|
+
### Modelo de IA por ação (`ai.models` no `.spec-wave.json`)
|
|
234
|
+
|
|
235
|
+
Cada ação de IA (`spec`, `plan`, `decompose`, `critique`) pode usar um modelo próprio, com fallback em `ai.model` e depois no default do provider:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"ai": {
|
|
240
|
+
"provider": "anthropic",
|
|
241
|
+
"model": "claude-sonnet-4-6",
|
|
242
|
+
"models": {
|
|
243
|
+
"plan": "claude-opus-4-1",
|
|
244
|
+
"critique": "claude-opus-4-1"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Edite o bloco `ai` no `.spec-wave.json` (e commite) — o `doctor` mostra o provider, o modelo e os overrides de `ai.models` resolvidos.
|
|
170
251
|
|
|
171
252
|
---
|
|
172
253
|
|
|
@@ -232,6 +313,8 @@ Crie um work item tipado (Initiative/Epic/Feature/Story/Task/...) já adicionado
|
|
|
232
313
|
|
|
233
314
|
**Hierarquia típica:** Initiative → Epic → Feature → Story → Task. A **Initiative** é o nó raiz e agrupa Epics. Use `--parent <n>` para criar como sub-issue do nível acima (ex.: um Epic filho de uma Initiative, ou uma Story filha de uma Feature). O GitHub mostra o parent na issue filha e vice-versa; a CLI ainda grava `Parent: #N` no corpo.
|
|
234
315
|
|
|
316
|
+
> **Spike é movido manualmente:** o spec-wave **nunca** avança a Etapa de um Spike automaticamente (nem no `implement`, nem nas Actions de Code Review/QA). O Spike entra no board em 📥 Backlog e o **usuário** o move à mão pelas etapas. Não mova a Etapa de um Spike por conta própria — a não ser que o usuário peça explicitamente.
|
|
317
|
+
|
|
235
318
|
**Passos:**
|
|
236
319
|
1. Pergunte ao usuário: tipo (initiative/epic/feature/story/task/...), título (sem prefixo), descrição e se há uma issue **pai** (número). **Prioridade e área são opcionais**: só as inclua se o usuário pedir explicitamente. **Nunca atribua uma prioridade por conta própria** — se o usuário não informou, **omita `--priority`** e a prioridade fica `null` (sem prioridade) no board.
|
|
237
320
|
2. Execute o comando com os parâmetros coletados (inclua **apenas** as flags que o usuário forneceu):
|
|
@@ -267,14 +350,17 @@ Remove a configuração do spec-wave do repositório (labels, arquivos `.github`
|
|
|
267
350
|
|
|
268
351
|
Inicia a geração da **especificação funcional** para uma Feature. É o **primeiro** passo do ciclo de documentos (antes do plano técnico).
|
|
269
352
|
|
|
353
|
+
> **Apenas Features.** spec/plan **não** são gerados para **Spike, RFC ou Bug** — se a label for adicionada a um desses, o Action pula a geração, remove a label e comenta. Não use `/spec-wave spec|plan` nesses tipos.
|
|
354
|
+
|
|
270
355
|
**Passos:**
|
|
271
|
-
1.
|
|
356
|
+
1. Confirme que a issue é uma **Feature** (spec/plan não se aplicam a Spike/RFC/Bug).
|
|
357
|
+
2. Adicione a label de gatilho:
|
|
272
358
|
```bash
|
|
273
359
|
gh issue edit <número> --add-label "spec-wave:spec"
|
|
274
360
|
```
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
361
|
+
3. Informe: "Label `spec-wave:spec` adicionada. O GitHub Action `generate-spec.yml` irá gerar o `spec.md` automaticamente."
|
|
362
|
+
4. Após a conclusão, ofereça revisar o spec.md gerado em `docs/features/<slug>/spec.md`.
|
|
363
|
+
5. Próximo passo: gerar o plano técnico — mova para **📋 Plan** e use `/spec-wave plan <número>`.
|
|
278
364
|
|
|
279
365
|
---
|
|
280
366
|
|
|
@@ -364,22 +450,28 @@ Valida que spec.md e plan.md estão completos e a Feature pode avançar.
|
|
|
364
450
|
```
|
|
365
451
|
2. Informe: "Validação iniciada. O workflow verificará se spec.md e plan.md contêm todas as seções obrigatórias."
|
|
366
452
|
3. Se a validação falhar, o workflow comentará os problemas na issue e adicionará automaticamente `spec-wave:spec`. Informe o usuário para corrigir e tentar novamente.
|
|
367
|
-
4. Se
|
|
453
|
+
4. **Se a issue tiver a label `spec-wave:critique-failed`**, a validação falha de imediato: a crítica adversarial apontou contradições graves (comentário 🔎 na issue). Siga o fluxo de resolução da seção *Crítica adversarial*: corrigir os documentos → remover a label → re-aplicar `spec-wave:ready`.
|
|
454
|
+
5. Se passar, oriente: "Feature validada! Mova o card para **✅ Ready** e depois para **📋 Backlog Técnico** para iniciar a decomposição."
|
|
368
455
|
|
|
369
456
|
---
|
|
370
457
|
|
|
371
458
|
### `/spec-wave decompose <número-da-issue>`
|
|
372
459
|
|
|
373
|
-
Decompõe
|
|
460
|
+
Decompõe automaticamente. Aplica-se a **dois tipos**:
|
|
461
|
+
- **Feature** → gera **Stories** (cada uma com suas **Tasks**), a partir de `spec.md` + `plan.md`.
|
|
462
|
+
- **RFC** → gera **Tasks diretamente** (sem Stories), a partir da descrição do RFC.
|
|
463
|
+
|
|
464
|
+
Para qualquer outro tipo (Spike, Bug, Story, Task, …) o Action **recusa** e comenta.
|
|
374
465
|
|
|
375
466
|
**Passos:**
|
|
376
|
-
1.
|
|
467
|
+
1. Para **Feature**: confirme que está em **✅ Ready** (spec.md e plan.md validados). Para **RFC**: basta a descrição estar completa (RFC não usa spec/plan).
|
|
377
468
|
2. Adicione a label de decomposição:
|
|
378
469
|
```bash
|
|
379
470
|
gh issue edit <número> --add-label "spec-wave:decompose"
|
|
380
471
|
```
|
|
381
|
-
3. Informe: "Decomposição iniciada
|
|
382
|
-
4. Após a conclusão, as issues filhas aparecerão como comentário na
|
|
472
|
+
3. Informe: "Decomposição iniciada — Feature gera Stories+Tasks; RFC gera Tasks."
|
|
473
|
+
4. Após a conclusão, as issues filhas aparecerão como comentário na issue pai, junto com o comentário 🔎 da crítica adversarial. As Stories geradas trazem a linha `Depende de: #N` (+ relação *blocked by*) — use `npx @spec-wave/cli order <número>` para ver a ordem de execução.
|
|
474
|
+
5. A issue recebe a label `spec-wave:decomposed` (guard de idempotência): rodar de novo **não** duplica as issues. Para forçar um re-decompose, siga a seção *Guard de idempotência*.
|
|
383
475
|
|
|
384
476
|
---
|
|
385
477
|
|
|
@@ -396,7 +488,7 @@ Aciona o spec-kit para implementar uma **Story** (todas as suas Tasks) ou uma **
|
|
|
396
488
|
npx @spec-wave/cli implement <número> --dry-run
|
|
397
489
|
```
|
|
398
490
|
3. Mostre ao usuário o contexto montado em `.spec-wave/implement-<número>.md` e o comando. Esse arquivo contém as **instruções de execução sequencial**: implemente as Tasks **uma por vez** — mova a task para **🚧 Desenvolvimento** só ao iniciá-la e para **🎉 Done** ao concluí-la, antes de passar para a próxima. **Nunca** coloque várias tasks em "in progress" ao mesmo tempo.
|
|
399
|
-
4. **Se você (agente) for implementar diretamente** (sem `specKit.command`): siga o contexto task por task. Para cada task:
|
|
491
|
+
4. **Se você (agente) for implementar diretamente** (sem `specKit.command`): siga o contexto task por task. Para cada task: `npx @spec-wave/cli task start <n>` ao iniciar (Etapa 🚧 Desenvolvimento + Status In Progress) e `npx @spec-wave/cli task done <n>` ao concluir (Etapa 🎉 Done + Status Done) — **prefira esses comandos a mutações GraphQL/`gh` manuais**: eles embutem as regras do board (Etapa nunca retrocede; uma task In Progress por vez). Se o contexto trouxer **aviso de dependência pendente** (a issue depende de outra não concluída), confirme com o usuário antes de seguir. **Ao concluir toda a Story**: faça o commit, abra o PR e mova a Story com `npx @spec-wave/cli story review <n>` (Etapa 👀 Code Review, Status → Todo) — as Tasks já estão em 🎉 Done. A **Feature só avança** quando **TODAS as suas Stories** já estiverem em Code Review — se houver Story pendente, deixe a Feature em 🚧 Desenvolvimento. Lembre: Etapa só avança (nunca volta); Status é o progresso dentro da etapa.
|
|
400
492
|
5. Se o usuário aprovar e o spec-kit estiver configurado, rode sem `--dry-run`:
|
|
401
493
|
```bash
|
|
402
494
|
npx @spec-wave/cli implement <número>
|
|
@@ -4,6 +4,13 @@ on:
|
|
|
4
4
|
issues:
|
|
5
5
|
types: [labeled]
|
|
6
6
|
|
|
7
|
+
# O evento `labeled` pode redisparar (re-add da label, retry de runner):
|
|
8
|
+
# o concurrency serializa runs da mesma issue e o guard de idempotência
|
|
9
|
+
# (label spec-wave:decomposed) descarta o run enfileirado.
|
|
10
|
+
concurrency:
|
|
11
|
+
group: spec-wave-decompose-${{ github.event.issue.number }}
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
7
14
|
jobs:
|
|
8
15
|
decompose:
|
|
9
16
|
if: >
|
package/src/ui/wizard.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { AI_PROVIDERS, getProvider, DEFAULT_PROVIDER } from '../config.mjs';
|
|
3
|
+
import { AI_PROVIDERS, getProvider, DEFAULT_PROVIDER, WORKFLOW_FILES, ISSUE_TEMPLATE_FILES } from '../config.mjs';
|
|
4
4
|
|
|
5
5
|
export async function runWizard() {
|
|
6
6
|
p.intro('spec-wave — configuração do fluxo spec-driven');
|
|
@@ -62,7 +62,7 @@ export async function runWizard() {
|
|
|
62
62
|
|
|
63
63
|
confirm: ({ results }) =>
|
|
64
64
|
p.confirm({
|
|
65
|
-
message: `Configurar ${results.repo} com GitHub Project "${results.projectTitle}"?\n - 12 colunas kanban\n - 5 campos customizados\n - 15 labels\n -
|
|
65
|
+
message: `Configurar ${results.repo} com GitHub Project "${results.projectTitle}"?\n - 12 colunas kanban\n - 5 campos customizados\n - 15 labels\n - ${WORKFLOW_FILES.length} workflows + ${ISSUE_TEMPLATE_FILES.length} issue templates`,
|
|
66
66
|
initialValue: true,
|
|
67
67
|
}),
|
|
68
68
|
},
|