@spec-wave/cli 0.17.0 → 0.18.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,7 @@ import {
7
7
  import { detectIssueType } from '../lib/issue-type.mjs';
8
8
  import {
9
9
  allowsSpecPlan, SPEC_PLAN_EXCLUDED_TYPES, TARGET_LANGUAGE,
10
- LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_RISK_ACCEPTED,
10
+ LABEL_CRITIQUE, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_RISK_ACCEPTED,
11
11
  DEFAULT_MAX_CRITIQUE_ATTEMPTS, labelNames,
12
12
  } from '../config.mjs';
13
13
  import { generateDocument } from '../lib/claude.mjs';
@@ -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.17.0",
4
+ "version": "0.18.1",
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/regere → remova `spec-wave:critique-failed` → reaplique `spec-wave:ready` |
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 }}