@spec-wave/cli 0.17.0 → 0.18.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
CHANGED
|
@@ -170,6 +170,15 @@ program
|
|
|
170
170
|
await generatePlan(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
program
|
|
174
|
+
.command('critique')
|
|
175
|
+
.description('Re-critica o plan.md COMO ESTÁ, sem regerar — para depois de corrigi-lo à mão')
|
|
176
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
177
|
+
.action(async (options) => {
|
|
178
|
+
const { critique } = await import('../src/commands/generate-plan.mjs');
|
|
179
|
+
await critique(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
180
|
+
});
|
|
181
|
+
|
|
173
182
|
program
|
|
174
183
|
.command('generate-spec')
|
|
175
184
|
.description('Gera spec.md para uma Feature — roda no GitHub Action ou localmente')
|
package/package.json
CHANGED
|
@@ -244,3 +244,56 @@ export async function generatePlan({ issueNumber }) {
|
|
|
244
244
|
await recordUsage({ token, owner, repo, issueNumber: parseInt(issueNumber, 10), entries: usageEntries });
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Roda a crítica adversarial sobre o `plan.md` COMO ESTÁ, sem regerar.
|
|
250
|
+
*
|
|
251
|
+
* Existe porque o único gatilho do plano regenerava o documento. O SKILL manda
|
|
252
|
+
* "corrija o plan.md e reaplique o gatilho" — e o gatilho descartava a correção
|
|
253
|
+
* feita à mão, gerando outro plano do zero. Pior: com o plano regenerado, as
|
|
254
|
+
* decisões que o Tech Leader tomou sobre os findings anteriores deixam de casar
|
|
255
|
+
* (os achados são outros), e o portão volta à estaca zero.
|
|
256
|
+
*
|
|
257
|
+
* Mesma semântica que o `decompose` já tinha para o rascunho: documento
|
|
258
|
+
* presente é criticado como está. Para gerar outro plano do zero, o caminho
|
|
259
|
+
* continua sendo `spec-wave:plan`.
|
|
260
|
+
*/
|
|
261
|
+
export async function critique({ issueNumber }) {
|
|
262
|
+
const token = await resolveToken();
|
|
263
|
+
const { owner, repo, root, config, mode } = resolveFlowContext({ command: 'critique' });
|
|
264
|
+
console.log(`Modo de execução: ${mode}`);
|
|
265
|
+
|
|
266
|
+
const n = parseInt(issueNumber, 10);
|
|
267
|
+
const issue = await getIssue(token, owner, repo, n);
|
|
268
|
+
const type = detectIssueType(issue);
|
|
269
|
+
if (!allowsSpecPlan(type)) {
|
|
270
|
+
console.log(`Issue #${n} é ${type}: não usa plan.md, nada a criticar.`);
|
|
271
|
+
await removeLabel(token, owner, repo, n, LABEL_CRITIQUE).catch(() => {});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const featureDir = resolveFromRoot(root, `docs/features/${slugify(issue.title)}`);
|
|
276
|
+
const planPath = path.join(featureDir, 'plan.md');
|
|
277
|
+
if (!existsSync(planPath)) {
|
|
278
|
+
console.log('plan.md ainda não existe — gere o plano antes de criticá-lo.');
|
|
279
|
+
await removeLabel(token, owner, repo, n, LABEL_CRITIQUE).catch(() => {});
|
|
280
|
+
await commentOnIssue(token, owner, repo, n,
|
|
281
|
+
'ℹ️ **Nada a criticar:** o `plan.md` ainda não foi gerado. ' +
|
|
282
|
+
'Aplique `spec-wave:plan` para gerá-lo.').catch(() => {});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const specPath = path.join(featureDir, 'spec.md');
|
|
286
|
+
|
|
287
|
+
const issueLabels = labelNames(issue.labels || []);
|
|
288
|
+
await critiquePlan({
|
|
289
|
+
token, owner, repo, issueNumber: n,
|
|
290
|
+
spec: existsSync(specPath) ? readFileSync(specPath, 'utf-8') : null,
|
|
291
|
+
plan: readFileSync(planPath, 'utf-8'),
|
|
292
|
+
techContextYaml: buildTechContext({ issueBody: issue.body || '' }).yaml,
|
|
293
|
+
labels: issueLabels, config, usage: [],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// O gatilho sai SEMPRE: `issues: [labeled]` não redispara com a label ainda
|
|
297
|
+
// aplicada, então mantê-la deixaria a issue sem como pedir outra crítica.
|
|
298
|
+
await removeLabel(token, owner, repo, n, LABEL_CRITIQUE).catch(() => {});
|
|
299
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -338,10 +338,15 @@ export const LABEL_NEEDS_HUMAN = 'spec-wave:needs-human';
|
|
|
338
338
|
// depois que a crítica passa: sem ela, o risco aceito some da vista assim que a
|
|
339
339
|
// rodada seguinte roda limpa, e ninguém mais sabe que houve uma decisão.
|
|
340
340
|
export const LABEL_RISK_ACCEPTED = 'spec-wave:risk-accepted';
|
|
341
|
+
// Gatilho de re-crítica do plano. Separado de `spec-wave:plan` porque aquele
|
|
342
|
+
// REGERA o documento: reaplicá-lo depois de corrigir o plan.md à mão descarta
|
|
343
|
+
// a correção — e troca os findings, fazendo as decisões do TL deixarem de casar.
|
|
344
|
+
export const LABEL_CRITIQUE = 'spec-wave:critique';
|
|
341
345
|
|
|
342
346
|
export const TRIGGER_LABELS = [
|
|
343
347
|
{ name: 'spec-wave:spec', color: 'BFD4F2', description: 'Gerar spec.md via GitHub Action' },
|
|
344
348
|
{ name: 'spec-wave:plan', color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
|
|
349
|
+
{ name: LABEL_CRITIQUE, color: 'BFD4F2', description: 'Re-criticar o plan.md COMO ESTÁ, sem regerar' },
|
|
345
350
|
{ name: 'spec-wave:ready', color: '0E8A16', description: 'Validar spec+plan e mover para Ready' },
|
|
346
351
|
{ name: 'spec-wave:plan-approved', color: '0E8A16', description: 'Spec+plan validados com sucesso' },
|
|
347
352
|
{ name: LABEL_DECOMPOSE, color: 'BFD4F2', description: 'Gerar/re-criticar o rascunho da decomposição (decomposition.md)' },
|
|
@@ -385,6 +390,7 @@ export function labelNames(issueOrLabels) {
|
|
|
385
390
|
export const WORKFLOW_FILES = [
|
|
386
391
|
'generate-bug.yml',
|
|
387
392
|
'generate-plan.yml',
|
|
393
|
+
'critique.yml',
|
|
388
394
|
'generate-spec.yml',
|
|
389
395
|
'validate.yml',
|
|
390
396
|
'decompose.yml',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.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",
|
|
@@ -98,6 +98,7 @@ Essa é a sequência completa, mas **cada tipo de artefato percorre só um trech
|
|
|
98
98
|
Labels de gatilho:
|
|
99
99
|
- `spec-wave:spec` → dispara `generate-spec.yml` → gera `spec.md` (especificação funcional, primeiro)
|
|
100
100
|
- `spec-wave:plan` → dispara `generate-plan.yml` → gera `plan.md` (plano técnico, a partir da spec)
|
|
101
|
+
- `spec-wave:critique` → dispara `critique.yml` → **re-critica o `plan.md` COMO ESTÁ**, sem regerar. É o gatilho de retomada depois de corrigir o plano à mão — `spec-wave:plan` geraria outro do zero, descartando a correção.
|
|
101
102
|
- `spec-wave:ready` → dispara `validate.yml` → valida ambos os arquivos
|
|
102
103
|
- `spec-wave:decompose` → dispara `decompose.yml` → gera (ou **re-critica**) o **rascunho** em `decomposition.md`. **Não cria issue nenhuma.**
|
|
103
104
|
- `spec-wave:decompose-apply` → dispara o mesmo workflow em modo aplicação → cria as Stories e Tasks **a partir do rascunho revisado**
|
|
@@ -269,7 +270,7 @@ A saída da crítica é **estruturada e validada por schema**: `severity` só ac
|
|
|
269
270
|
|
|
270
271
|
| Reprovou depois de | O que corrigir | Como retomar |
|
|
271
272
|
|--------------------|----------------|--------------|
|
|
272
|
-
| `generate-plan` | `plan.md` (ou a `spec.md` que o embasa) | corrija
|
|
273
|
+
| `generate-plan` | `plan.md` (ou a `spec.md` que o embasa) | corrija → remova `spec-wave:critique-failed` → reaplique **`spec-wave:critique`** (re-critica o arquivo como está). `spec-wave:ready` apenas valida, **não** re-critica; e `spec-wave:plan` **regera** o plano, descartando a correção. |
|
|
273
274
|
| `decompose` (rascunho) | **`decomposition.md`** — os achados citam **`Story N`** e **`Task N.M`**, que são os títulos desse arquivo | edite o arquivo e commite → remova `spec-wave:critique-failed` → reaplique `spec-wave:decompose` (ele **critica o arquivo como está**, sem regerar) |
|
|
274
275
|
|
|
275
276
|
> ⚠️ No contexto do `decompose`, os achados são sobre as **Stories propostas**, não sobre o `plan.md`. Corrigir o plan não influencia o rascunho já gravado — edite o `decomposition.md` diretamente. Foi exatamente essa confusão que fazia o ciclo não convergir.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Critique Plan
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [labeled]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: spec-wave-critique-${{ github.event.issue.number }}
|
|
9
|
+
cancel-in-progress: false
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
critique:
|
|
13
|
+
if: >
|
|
14
|
+
github.event.label.name == 'spec-wave:critique' &&
|
|
15
|
+
contains(github.event.issue.title, '[FEATURE]')
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
issues: write
|
|
19
|
+
# read: a crítica LÊ o plan.md do checkout e não escreve arquivo nenhum —
|
|
20
|
+
# ao contrário do generate-plan, que commita o documento gerado.
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: '24'
|
|
31
|
+
|
|
32
|
+
- name: Critique plan.md as-is
|
|
33
|
+
run: npx @spec-wave/cli@{{CLI_VERSION}} critique --issue-number ${{ github.event.issue.number }}
|
|
34
|
+
env:
|
|
35
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
37
|
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
38
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|