@spec-wave/cli 0.27.0 → 0.28.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/package.json +1 -1
- package/src/api/github-rest.mjs +27 -0
- package/src/cli.mjs +12 -0
- package/src/commands/doctor.mjs +40 -16
- package/src/commands/implement.mjs +7 -2
- package/src/commands/install-skill.mjs +18 -8
- package/src/commands/preflight.mjs +322 -0
- package/src/commands/update.mjs +143 -12
- package/src/lib/pr-branch.mjs +96 -7
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/README.md +5 -0
- package/src/plugin/skills/preparar-feature/SKILL.md +245 -0
- package/src/plugin/skills/preparar-feature/reference/critica.md +88 -0
- package/src/plugin/skills/preparar-specs/SKILL.md +171 -0
- package/src/plugin/skills/preparar-specs/reference/armadilhas.md +209 -0
- package/src/plugin/skills/preparar-specs/reference/revisao.md +107 -0
- package/src/plugin/skills/update/SKILL.md +10 -4
- package/src/plugin/skills/workflow/SKILL.md +6 -1
- package/src/templates/skill/SKILL.md +6 -3
package/src/commands/update.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from '../api/github-rest.mjs';
|
|
13
13
|
import {
|
|
14
14
|
resolveBranchName, composePrTitle, composePrBody, buildCommitMessage,
|
|
15
|
-
decideConfigInPr, explainGitWriteError,
|
|
15
|
+
decideConfigInPr, decideSkillInPr, composeConfigCleanupHint, explainGitWriteError,
|
|
16
16
|
} from '../lib/pr-branch.mjs';
|
|
17
17
|
// MESMO readTemplate do init: resolve {{CLI_VERSION}} antes da comparação byte a
|
|
18
18
|
// byte com o remoto. Se só o init resolvesse, todo update veria os workflows
|
|
@@ -20,10 +20,11 @@ import {
|
|
|
20
20
|
import { readTemplate } from '../lib/templates.mjs';
|
|
21
21
|
import {
|
|
22
22
|
TARGETS, SKILL_SOURCE, CLI_VERSION, parseSkill, renderContent,
|
|
23
|
-
mergeAgentsFile, resolveDest, isDetected, skillCopyReason, versionBanner,
|
|
23
|
+
mergeAgentsFile, mergeAgentsContent, resolveDest, isDetected, skillCopyReason, versionBanner,
|
|
24
24
|
} from './install-skill.mjs';
|
|
25
25
|
import { planPluginSkillFiles } from '../lib/plugin-skills.mjs';
|
|
26
26
|
import { findConfigPath } from '../lib/project-root.mjs';
|
|
27
|
+
import { currentGitBranch } from '../lib/repo-links.mjs';
|
|
27
28
|
|
|
28
29
|
// Arquivos do repo gerenciados pela CLI (comparados com o template empacotado).
|
|
29
30
|
const REPO_FILES = [
|
|
@@ -68,6 +69,41 @@ function applySkill(job) {
|
|
|
68
69
|
writeFileSync(job.dest.path, content, 'utf-8');
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Arquivos que uma atualização de skill grava, em caminhos RELATIVOS à raiz do
|
|
74
|
+
* repositório — a chave para perguntar à base se aquela cópia é versionada.
|
|
75
|
+
*
|
|
76
|
+
* Devolve [] quando o destino cai FORA da raiz, e é o que dispensa um caso
|
|
77
|
+
* especial para `--global`: `~/.claude/skills/...` e o `~/.gemini/AGENTS.md` do
|
|
78
|
+
* Antigravity jamais são conteúdo de repositório, e `path.relative` já diz isso.
|
|
79
|
+
*
|
|
80
|
+
* O formato `agents` (AGENTS.md) devolve `block` em vez de `content`: o conteúdo
|
|
81
|
+
* final depende do que existe na BASE — mesclar no arquivo local arrastaria para
|
|
82
|
+
* dentro do PR as edições não commitadas do desenvolvedor.
|
|
83
|
+
*
|
|
84
|
+
* @param {{dest: {path: string, format: string}, desired?: string}} job
|
|
85
|
+
* @param {string} repoRoot
|
|
86
|
+
* @returns {Array<{ relPath: string, content?: string, block?: string }>}
|
|
87
|
+
*/
|
|
88
|
+
export function skillRepoTargets(job, repoRoot) {
|
|
89
|
+
const items = job.dest.format === 'skills-dir'
|
|
90
|
+
? planPluginSkillFiles(job.dest.path, CLI_VERSION, versionBanner)
|
|
91
|
+
.map(f => ({ absPath: f.path, content: f.content }))
|
|
92
|
+
: [{
|
|
93
|
+
absPath: job.dest.path,
|
|
94
|
+
...(job.dest.format === 'agents' ? { block: job.desired } : { content: job.desired }),
|
|
95
|
+
}];
|
|
96
|
+
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const { absPath, ...rest } of items) {
|
|
99
|
+
const rel = path.relative(repoRoot, absPath);
|
|
100
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) continue;
|
|
101
|
+
// A API do GitHub fala POSIX; path.relative devolve no separador do SO.
|
|
102
|
+
out.push({ ...rest, relPath: rel.split(path.sep).join('/') });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
/**
|
|
72
108
|
* Compara ALL_LABELS com as labels do repo (função PURA — testável).
|
|
73
109
|
*
|
|
@@ -325,6 +361,70 @@ export async function update(options = {}) {
|
|
|
325
361
|
}
|
|
326
362
|
}
|
|
327
363
|
|
|
364
|
+
// 2d) Skill: a BASE versiona alguma destas cópias?
|
|
365
|
+
//
|
|
366
|
+
// Até aqui o update gravava a skill em disco e o corpo do PR AFIRMAVA ao
|
|
367
|
+
// revisor que ela "não faz parte do repositório" — premissa fixa. Em repos que
|
|
368
|
+
// versionam `.claude/skills/spec-wave/SKILL.md` (ou `.agents/skills/`, ou o
|
|
369
|
+
// AGENTS.md) isso era falso duas vezes: sobrava um commit manual a cada bump, e
|
|
370
|
+
// até ele o repositório seguia distribuindo a skill da versão anterior para
|
|
371
|
+
// quem clonasse. A checagem é a MESMA do .spec-wave.json — perguntar à base —,
|
|
372
|
+
// e a política também: só entra no PR o que o projeto já versiona.
|
|
373
|
+
let skillPrFiles = []; // [{ relPath, content, reason, agent }]
|
|
374
|
+
// agente → motivo da PRIMEIRA cópia incluída. É o motivo que o resumo mostra:
|
|
375
|
+
// dizer "versionada no repo" para uma cópia que entrou por `--skill-in-pr`
|
|
376
|
+
// seria repetir, em miniatura, a premissa que este código veio corrigir.
|
|
377
|
+
const skillPrAgents = new Map();
|
|
378
|
+
// `skillRepoTargets` já devolve vazio para o que mora fora da raiz, o que cobre
|
|
379
|
+
// o `--global` inteiro sem um caso especial: nada a perguntar, nada a gastar.
|
|
380
|
+
const skillCandidates = prMode
|
|
381
|
+
? skillJobs
|
|
382
|
+
.map(job => ({ job, targets: skillRepoTargets(job, path.dirname(configPath)) }))
|
|
383
|
+
.filter(c => c.targets.length)
|
|
384
|
+
: [];
|
|
385
|
+
if (skillCandidates.length) {
|
|
386
|
+
const tk = await getToken();
|
|
387
|
+
const s = p.spinner();
|
|
388
|
+
s.start('Verificando se o repositório versiona a skill...');
|
|
389
|
+
try {
|
|
390
|
+
for (const { job, targets } of skillCandidates) {
|
|
391
|
+
// Em paralelo: o destino do Codex são 20+ arquivos, e um GET por vez
|
|
392
|
+
// transformaria a checagem na parte mais lenta do comando.
|
|
393
|
+
const remotes = await Promise.all(
|
|
394
|
+
targets.map(t => getFileContent(tk, owner, repo, t.relPath, base))
|
|
395
|
+
);
|
|
396
|
+
targets.forEach((t, i) => {
|
|
397
|
+
const remote = remotes[i];
|
|
398
|
+
const desired = t.block !== undefined
|
|
399
|
+
? mergeAgentsContent(remote ?? '', t.block)
|
|
400
|
+
: t.content;
|
|
401
|
+
const d = decideSkillInPr({ remote, desired, force: options.skillInPr });
|
|
402
|
+
if (!d.included) return;
|
|
403
|
+
skillPrFiles.push({
|
|
404
|
+
relPath: t.relPath,
|
|
405
|
+
content: desired,
|
|
406
|
+
reason: remote == null ? 'ausente' : 'desatualizado',
|
|
407
|
+
agent: job.target.name,
|
|
408
|
+
});
|
|
409
|
+
if (!skillPrAgents.has(job.target.name)) skillPrAgents.set(job.target.name, d.reason);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
s.stop(skillPrFiles.length
|
|
413
|
+
? `Skill versionada no repositório: ${skillPrFiles.length} arquivo(s) vão no Pull Request.`
|
|
414
|
+
: 'A skill não é versionada neste repositório — segue apenas local.');
|
|
415
|
+
} catch (err) {
|
|
416
|
+
// Falhar aqui não pode custar o update inteiro: sem a resposta da base, a
|
|
417
|
+
// skill volta a ser só local — o comportamento anterior, agora explícito.
|
|
418
|
+
s.stop('');
|
|
419
|
+
p.log.warn(`Não foi possível checar a skill na base: ${err.message} — ela seguirá apenas local.`);
|
|
420
|
+
skillPrFiles = [];
|
|
421
|
+
skillPrAgents.clear();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const skillLocalAgents = skillJobs
|
|
425
|
+
.map(j => j.target.name)
|
|
426
|
+
.filter(n => !skillPrAgents.has(n));
|
|
427
|
+
|
|
328
428
|
// Decisão preliminar sobre o config. O conteúdo regenerado ainda não existe;
|
|
329
429
|
// `willRegenerate` cobre isso, porque o regenerado SEMPRE difere do remoto
|
|
330
430
|
// (refreshedAt muda a cada execução). Na aplicação a decisão é recalculada com
|
|
@@ -355,21 +455,39 @@ export async function update(options = {}) {
|
|
|
355
455
|
const lines = [];
|
|
356
456
|
if (skillJobs.length) {
|
|
357
457
|
lines.push(chalk.bold('Skill:'));
|
|
358
|
-
for (const j of skillJobs)
|
|
458
|
+
for (const j of skillJobs) {
|
|
459
|
+
const destino = skillPrAgents.has(j.target.name)
|
|
460
|
+
? chalk.dim(` → vai no PR: ${skillPrAgents.get(j.target.name)}`)
|
|
461
|
+
: '';
|
|
462
|
+
lines.push(` ${chalk.yellow('↻')} ${j.target.name} (${j.reason})${destino}\n ${chalk.dim(j.dest.path)}`);
|
|
463
|
+
}
|
|
359
464
|
}
|
|
360
465
|
if (configStale) {
|
|
361
466
|
lines.push(chalk.bold('Config local:'));
|
|
362
467
|
lines.push(` ${chalk.yellow('↻')} ${CONFIG_FILE} — ${configStale.reason}` +
|
|
363
468
|
(configStale.canApply ? '' : chalk.dim(' (sem project.id — rode `init` sem --skip-project)')));
|
|
364
469
|
}
|
|
470
|
+
// Cabeçalho ÚNICO no modo PR: workflows, skill e config viajam no mesmo commit,
|
|
471
|
+
// e três títulos para um commit só sugeriam três destinos diferentes.
|
|
472
|
+
let prHeaderShown = false;
|
|
473
|
+
const prHeader = () => {
|
|
474
|
+
if (prHeaderShown) return;
|
|
475
|
+
prHeaderShown = true;
|
|
476
|
+
lines.push(chalk.bold(`Pull Request (${branch} → ${base}):`));
|
|
477
|
+
};
|
|
365
478
|
if (repoFiles.length) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
: chalk.bold('Arquivos do repo:'));
|
|
479
|
+
if (prMode) prHeader();
|
|
480
|
+
else lines.push(chalk.bold('Arquivos do repo:'));
|
|
369
481
|
for (const f of repoFiles) lines.push(` ${chalk.yellow('↻')} ${f.repoPath} (${f.reason})`);
|
|
370
482
|
}
|
|
483
|
+
if (skillPrFiles.length) {
|
|
484
|
+
prHeader();
|
|
485
|
+
for (const f of skillPrFiles) {
|
|
486
|
+
lines.push(` ${chalk.yellow('↻')} ${f.relPath} (${f.reason} — skill ${f.agent})`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
371
489
|
if (prMode && configDecision.included) {
|
|
372
|
-
|
|
490
|
+
prHeader();
|
|
373
491
|
lines.push(` ${chalk.yellow('↻')} ${CONFIG_FILE} (${configDecision.reason})`);
|
|
374
492
|
}
|
|
375
493
|
if (prMode && doConfig && !configDecision.included && configDecision.reason) {
|
|
@@ -388,7 +506,8 @@ export async function update(options = {}) {
|
|
|
388
506
|
p.note(lines.join('\n'), `${total} item(ns) desatualizado(s)`);
|
|
389
507
|
|
|
390
508
|
// --branch sem efeito: melhor dizer POR QUE do que criar uma branch inútil.
|
|
391
|
-
const prHasPayload = prMode
|
|
509
|
+
const prHasPayload = prMode
|
|
510
|
+
&& (repoFiles.length > 0 || skillPrFiles.length > 0 || configDecision.included);
|
|
392
511
|
if (prRequested && options.skipRepo) {
|
|
393
512
|
p.log.warn(
|
|
394
513
|
'--branch ignorado junto com --skip-repo: o Pull Request existe justamente para ' +
|
|
@@ -408,7 +527,7 @@ export async function update(options = {}) {
|
|
|
408
527
|
|
|
409
528
|
if (options.dryRun) {
|
|
410
529
|
if (prHasPayload) {
|
|
411
|
-
const n = repoFiles.length + (configDecision.included ? 1 : 0);
|
|
530
|
+
const n = repoFiles.length + skillPrFiles.length + (configDecision.included ? 1 : 0);
|
|
412
531
|
p.note(
|
|
413
532
|
`Branch: ${branch}${branchExists ? ' (já existe — o commit seria empilhado nela)' : ' (seria criada)'}\n` +
|
|
414
533
|
`Base: ${base}\n` +
|
|
@@ -516,6 +635,11 @@ export async function update(options = {}) {
|
|
|
516
635
|
const tk = await getToken();
|
|
517
636
|
const treeFiles = [
|
|
518
637
|
...repoFiles.map(f => ({ path: f.repoPath, reason: f.reason, content: f.local })),
|
|
638
|
+
...skillPrFiles.map(f => ({
|
|
639
|
+
path: f.relPath,
|
|
640
|
+
reason: `${f.reason} — skill ${f.agent}`,
|
|
641
|
+
content: f.content,
|
|
642
|
+
})),
|
|
519
643
|
...(finalConfigDecision.included
|
|
520
644
|
? [{
|
|
521
645
|
path: CONFIG_FILE,
|
|
@@ -562,7 +686,7 @@ export async function update(options = {}) {
|
|
|
562
686
|
files: treeFiles,
|
|
563
687
|
config: doConfig ? finalConfigDecision : null,
|
|
564
688
|
labels: labelResult,
|
|
565
|
-
skill:
|
|
689
|
+
skill: { inPr: [...skillPrAgents.keys()], local: skillLocalAgents },
|
|
566
690
|
}),
|
|
567
691
|
});
|
|
568
692
|
if (!pr) {
|
|
@@ -605,9 +729,16 @@ export async function update(options = {}) {
|
|
|
605
729
|
(prUrl ? ` Revise e faça o merge do Pull Request: ${prUrl}` : '') +
|
|
606
730
|
(!prMode && repoFiles.length ? ' Arquivos do repo foram commitados no remoto.' : '') +
|
|
607
731
|
(configPending ? ` COMMITE o ${CONFIG_FILE} — quem clona o repo (dev-agent, Actions) lê a versão commitada.` : '') +
|
|
732
|
+
// A dica de descarte depende da branch do CHECKOUT, não de premissa:
|
|
733
|
+
// `git checkout -- <arquivo>` restaura o que está no índice, e rodado de uma
|
|
734
|
+
// branch de trabalho reverte o arquivo para a versão ANTIGA dela — o oposto
|
|
735
|
+
// do que a mensagem promete.
|
|
608
736
|
(finalConfigDecision.included
|
|
609
|
-
? `
|
|
610
|
-
|
|
737
|
+
? ` ${composeConfigCleanupHint({
|
|
738
|
+
file: CONFIG_FILE,
|
|
739
|
+
base,
|
|
740
|
+
current: currentGitBranch(path.dirname(configPath)),
|
|
741
|
+
})}`
|
|
611
742
|
: '')
|
|
612
743
|
);
|
|
613
744
|
}
|
package/src/lib/pr-branch.mjs
CHANGED
|
@@ -130,7 +130,11 @@ export function buildCommitMessage({ version = CLI_VERSION, files = [] } = {}) {
|
|
|
130
130
|
* @param {{created: string[], updated: string[], removed: string[]}|null} [a.labels]
|
|
131
131
|
* labels EFETIVAMENTE aplicadas (não o diff detectado — o corpo não pode
|
|
132
132
|
* prometer o que falhou)
|
|
133
|
-
* @param {string[]} [a.skill]
|
|
133
|
+
* @param {string[]|{inPr?: string[], local?: string[]}} [a.skill] agentes cuja
|
|
134
|
+
* skill foi atualizada: `inPr` os que entraram no commit (a cópia da skill
|
|
135
|
+
* É versionada na base) e `local` os que ficaram só na máquina. Um array
|
|
136
|
+
* simples é lido como `local` — a forma antiga, de quando a skill nunca
|
|
137
|
+
* entrava no PR.
|
|
134
138
|
* @returns {string} markdown
|
|
135
139
|
*/
|
|
136
140
|
export function composePrBody({
|
|
@@ -173,14 +177,31 @@ export function composePrBody({
|
|
|
173
177
|
if (labels.removed.length) l.push(`- removidas (descontinuadas): ${labels.removed.map(n => `\`${n}\``).join(', ')}`);
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
|
|
180
|
+
// A skill não é mais declarada "fora do repositório" por premissa: quem chama
|
|
181
|
+
// consultou a base arquivo a arquivo (decideSkillInPr) e diz aqui o que
|
|
182
|
+
// realmente aconteceu. Afirmar "não faz parte do repositório" para um repo que
|
|
183
|
+
// versiona `.claude/skills/spec-wave/SKILL.md` mandava o revisor aprovar um PR
|
|
184
|
+
// incompleto — e deixava o repositório distribuindo a skill da versão anterior.
|
|
185
|
+
const skillInPr = Array.isArray(skill) ? [] : (skill?.inPr ?? []);
|
|
186
|
+
const skillLocal = Array.isArray(skill) ? skill : (skill?.local ?? []);
|
|
187
|
+
if (skillInPr.length || skillLocal.length) {
|
|
177
188
|
l.push('');
|
|
178
|
-
l.push('## Skill dos agentes
|
|
189
|
+
l.push('## Skill dos agentes');
|
|
179
190
|
l.push('');
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
if (skillInPr.length) {
|
|
192
|
+
l.push(
|
|
193
|
+
`Incluída neste PR, nos arquivos listados acima: ${skillInPr.map(n => `\`${n}\``).join(', ')}. ` +
|
|
194
|
+
`Esta cópia da skill é conteúdo do repositório — o merge em \`${base}\` é o que a ` +
|
|
195
|
+
'entrega a quem clonar.'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (skillLocal.length) {
|
|
199
|
+
if (skillInPr.length) l.push('');
|
|
200
|
+
l.push(
|
|
201
|
+
`Atualizada só na máquina local: ${skillLocal.map(n => `\`${n}\``).join(', ')}. ` +
|
|
202
|
+
'Esta cópia não é versionada no repositório e não faz parte deste PR.'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
l.push('');
|
|
@@ -235,6 +256,74 @@ export function decideConfigInPr({ remote, desired, willRegenerate = false, forc
|
|
|
235
256
|
return { included: true, reason: 'versionado na base e divergente do local' };
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Uma cópia da skill entra no PR? (função PURA)
|
|
261
|
+
*
|
|
262
|
+
* MESMA regra do .spec-wave.json, pelo mesmo motivo: onde o arquivo mora é
|
|
263
|
+
* decisão do projeto, e o update não pode mudá-la sozinho — nem passando a
|
|
264
|
+
* versionar o que ninguém commitou, nem deixando de atualizar o que o repositório
|
|
265
|
+
* já distribui. A diferença é que aqui não existe `willRegenerate`: o conteúdo
|
|
266
|
+
* desejado da skill é renderizado do pacote e já está pronto na hora de decidir.
|
|
267
|
+
*
|
|
268
|
+
* Vale por ARQUIVO, não por agente: o destino do Codex (`.agents/skills/`) são 20+
|
|
269
|
+
* arquivos, e nada garante que o repositório versione todos.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} [a]
|
|
272
|
+
* @param {string|null|undefined} [a.remote] conteúdo do arquivo na base (null/undefined = não versionado)
|
|
273
|
+
* @param {string|null} [a.desired] conteúdo que a instalação local grava
|
|
274
|
+
* @param {boolean|undefined} [a.force] undefined | true (--skill-in-pr) | false (--no-skill-in-pr)
|
|
275
|
+
* @returns {{ included: boolean, reason: string }}
|
|
276
|
+
*/
|
|
277
|
+
export function decideSkillInPr({ remote, desired, force } = {}) {
|
|
278
|
+
const versioned = remote !== null && remote !== undefined;
|
|
279
|
+
if (force === false) return { included: false, reason: '--no-skill-in-pr' };
|
|
280
|
+
if (!desired) return { included: false, reason: 'sem conteúdo local para enviar' };
|
|
281
|
+
if (force === true) {
|
|
282
|
+
return versioned
|
|
283
|
+
? { included: true, reason: '--skill-in-pr' }
|
|
284
|
+
: { included: true, reason: '--skill-in-pr (passa a versionar a skill)' };
|
|
285
|
+
}
|
|
286
|
+
if (!versioned) {
|
|
287
|
+
return {
|
|
288
|
+
included: false,
|
|
289
|
+
reason: 'não está versionada na base, segue apenas local ' +
|
|
290
|
+
'(use --skill-in-pr para versioná-la)',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (remote === desired) return { included: false, reason: 'já idêntica na base' };
|
|
294
|
+
return { included: true, reason: 'versionada na base e divergente do local' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Dica para descartar a cópia local de um arquivo que também foi no PR (PURA).
|
|
299
|
+
*
|
|
300
|
+
* `git checkout -- <arquivo>` restaura a versão do ÍNDICE, isto é, a do HEAD da
|
|
301
|
+
* branch que está no checkout. Ele só descarta uma cópia redundante quando o
|
|
302
|
+
* clone está na base; rodado de uma branch de trabalho, faz o oposto do que a
|
|
303
|
+
* mensagem promete — reverte o arquivo para a versão antiga daquela branch. A
|
|
304
|
+
* dica, portanto, depende da branch corrente, que a CLI já sabe consultar.
|
|
305
|
+
*
|
|
306
|
+
* @param {object} [a]
|
|
307
|
+
* @param {string} [a.file] caminho do arquivo, relativo à raiz
|
|
308
|
+
* @param {string} [a.base] branch base do Pull Request
|
|
309
|
+
* @param {string|null} [a.current] branch do clone local (null = detached/fora de git)
|
|
310
|
+
* @returns {string} frase única, para o `outro` do comando
|
|
311
|
+
*/
|
|
312
|
+
export function composeConfigCleanupHint({ file = CONFIG_FILE, base = 'main', current = null } = {}) {
|
|
313
|
+
const cabeca = `O ${file} local ficou igual ao do PR`;
|
|
314
|
+
if (current === base) {
|
|
315
|
+
return `${cabeca} — depois do merge, descarte a cópia local com ` +
|
|
316
|
+
`\`git checkout -- ${file}\` antes do \`git pull\`.`;
|
|
317
|
+
}
|
|
318
|
+
const onde = current
|
|
319
|
+
? `o checkout está em "${current}", e \`git checkout -- ${file}\` ali restauraria a ` +
|
|
320
|
+
'versão antiga dessa branch'
|
|
321
|
+
: `o checkout não está numa branch, e \`git checkout -- ${file}\` ali restauraria a ` +
|
|
322
|
+
'versão antiga do HEAD atual';
|
|
323
|
+
return `${cabeca} — ${onde}. Depois do merge, descarte a cópia local na base: ` +
|
|
324
|
+
`\`git switch ${base} && git checkout -- ${file}\`.`;
|
|
325
|
+
}
|
|
326
|
+
|
|
238
327
|
/**
|
|
239
328
|
* Traduz o erro cru da Git Data API para uma dica acionável (função PURA).
|
|
240
329
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.28.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",
|
package/src/plugin/README.md
CHANGED
|
@@ -18,6 +18,11 @@ carrega só a sua instrução.
|
|
|
18
18
|
| `plan` | Gera `plan.md` (2º documento) + `tech_context.yml` |
|
|
19
19
|
| `ready` | Valida spec + plan |
|
|
20
20
|
| `decompose` | Rascunho revisável → Stories/Tasks |
|
|
21
|
+
| `preparar-feature` | Orquestra: da spec até ✅ Ready, com Stories e Tasks criadas |
|
|
22
|
+
| `preparar-specs` | Orquestra: as specs de uma milestone inteira |
|
|
23
|
+
| `run` | Executa o próximo passo localmente (`run`/`mode`) |
|
|
24
|
+
| `bug` | Fluxo do Bug: `bug.md`, triagem, correção |
|
|
25
|
+
| `triage` | Tria um Bug: accept / reject / duplicate |
|
|
21
26
|
| `order` | Ordem topológica das Stories |
|
|
22
27
|
| `implement` | Etapa 🚧 Desenvolvimento |
|
|
23
28
|
| `task` | `start` / `done` de uma Task |
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-wave-preparar-feature
|
|
3
|
+
description: "Use para conduzir uma Feature do spec-wave pelo trecho que vai da spec pronta até ✅ Ready com as Stories e Tasks criadas no board: gera o plan.md, trata a crítica adversarial, valida, move a etapa, decompõe e confere o resultado. Gatilhos: 'gerar o plano', 'preparar a feature 12', 'deixar pronta para o dev', 'levar até ready', 'decompor a feature', 'roda o plan da #12'. Use mesmo quando o usuário nomear só um passo — os passos têm armadilhas encadeadas que só fazem sentido tratadas juntas. Para as specs de uma milestone inteira use preparar-specs; para um passo isolado e sem supervisão, as skills plan, ready ou decompose."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash(npx @spec-wave/cli@latest *)
|
|
6
|
+
- Bash(gh issue *)
|
|
7
|
+
- Bash(gh pr *)
|
|
8
|
+
- Bash(gh api *)
|
|
9
|
+
- Bash(git *)
|
|
10
|
+
- Read
|
|
11
|
+
- Edit
|
|
12
|
+
- Glob
|
|
13
|
+
- Grep
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Preparar Feature — de `spec.md` até ✅ Ready
|
|
17
|
+
|
|
18
|
+
Conduz uma issue `[FEATURE]` pelo trecho do fluxo que vai da spec pronta até as Stories e Tasks criadas e visíveis no board.
|
|
19
|
+
|
|
20
|
+
O valor desta skill não está em executar os passos — isso é fácil. Está em **saber o que verificar depois de cada um**. O fluxo tem pontos onde ele para em silêncio, e cada um já custou horas quando passou despercebido. Se você seguir só a sequência feliz, vai reportar sucesso sobre um estado quebrado.
|
|
21
|
+
|
|
22
|
+
**Contexto:** leia `.spec-wave.json` (Read). Ausente → skill **setup**.
|
|
23
|
+
|
|
24
|
+
## O ciclo, e por que ele não é uma sequência de comandos
|
|
25
|
+
|
|
26
|
+
Cada passo publica o documento num **Pull Request próprio** (`spec-wave/<issue>-<doc>`), e o passo seguinte **lê da branch base**. O merge não é arrumação — é pré-requisito:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
run --dry-run → run --yes → PR do documento → revisar → merge → git pull → próximo passo
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Pular o merge não dá erro confuso: o `run` para e explica (`⛔ pr-pending`). Mas se você tentar contornar com `--step`, aí sim regenera por cima da revisão em curso e paga o modelo de novo.
|
|
33
|
+
|
|
34
|
+
**Nada é gravado no seu checkout.** Não crie branch para "trabalhar nela", não commite documento à mão, não abra PR no fim — a CLI já faz tudo isso, uma vez por documento.
|
|
35
|
+
|
|
36
|
+
## Modos de supervisão
|
|
37
|
+
|
|
38
|
+
O padrão **para nos dois portões de julgamento** e pede aprovação: depois da crítica do plano, e antes do `decompose-apply`. Foi exatamente ali que a supervisão humana pegou defeitos reais — um plano que afirmava "esta feature não cria tabela" e trazia um `CREATE TABLE` logo abaixo, e uma task de E2E numa Story que não declarava depender do backend. A crítica automática passou pelos dois.
|
|
39
|
+
|
|
40
|
+
Se o usuário pedir para você analisar e corrigir sozinho, siga — mas **leia cada artefato mesmo assim**. Não parar não é o mesmo que não olhar: corrija, siga, e registre no relatório final o que mudou e por quê.
|
|
41
|
+
|
|
42
|
+
## Antes de começar
|
|
43
|
+
|
|
44
|
+
Verifique tudo isto e **pare com um diagnóstico** se algo falhar:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @spec-wave/cli@latest --version # >= 0.27.0
|
|
48
|
+
npx @spec-wave/cli@latest mode # actions ou local?
|
|
49
|
+
gh issue view <n> --json labels,title -q '{t:.title,l:[.labels[].name]}'
|
|
50
|
+
npx @spec-wave/cli@latest doctor
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**O modo decide como você aciona cada passo**, e os dois são legítimos:
|
|
54
|
+
|
|
55
|
+
| Modo | Aciona com | Observação |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `actions` | a label (`spec-wave:plan`, `:ready`, `:decompose`…) | roda no CI |
|
|
58
|
+
| `local` | `npx @spec-wave/cli@latest run <n>` | **não aplique label**: dispararia o Action e o passo rodaria duas vezes |
|
|
59
|
+
|
|
60
|
+
Se o repositório está em `local`, aplicar uma label não gera nada — o job é pulado pelo `if: vars.SPEC_WAVE_EXECUTION != 'local'` e o run aparece verde e `skipped`, sem erro e sem documento. É o sintoma mais silencioso do fluxo. O caminho contrário também engana: em `actions`, o `run` não recusa, apenas avisa.
|
|
61
|
+
|
|
62
|
+
Pare também se houver `spec-wave:critique-failed` ou `spec-wave:needs-human` — são portões humanos abertos de uma rodada anterior, e avançar por cima deles descarta a razão pela qual alguém parou.
|
|
63
|
+
|
|
64
|
+
O slug vem do título da issue. Confirme olhando `docs/features/`, não deduza.
|
|
65
|
+
|
|
66
|
+
### Onde começar depende do que já existe
|
|
67
|
+
|
|
68
|
+
Não assuma que a Feature chega do zero — outra pessoa pode ter avançado antes de você. O `run --dry-run` responde isso sozinho, e é sempre o primeiro comando:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx @spec-wave/cli@latest run <n> --dry-run
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Ele diz o passo pendente e o motivo, olhando o disco, a branch base **e** as branches de artefato. **Se `plan.md` já existe, nunca force `--step plan`** — ele regera o documento do zero e descarta o que houver, inclusive revisão manual. Para corrigir um plano existente, `--step critique`.
|
|
75
|
+
|
|
76
|
+
Leia também o último comentário 🔎 da issue; o marcador `<!-- spec-wave:critique kind=plan verdict=limpa -->` no topo diz o veredito sem você ter que interpretar o texto:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gh issue view <n> --comments --json comments -q '.comments[-1].body' | head -20
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Um caso que o marcador revela: um `plan.md` gerado por um backend sem suporte a saída estruturada sai completo, mas a crítica aborta — e o comentário registra que o documento **não foi auditado**. Plano nunca auditado indo para `ready` é o sucesso aparente sobre estado não verificado que esta skill existe para evitar.
|
|
83
|
+
|
|
84
|
+
### Os portões do `run`
|
|
85
|
+
|
|
86
|
+
O `run` explica e para, saindo com código 2. Não force sem entender:
|
|
87
|
+
|
|
88
|
+
| Portão | O que significa |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `trigger-pending` | label de gatilho na issue. Em modo local ela é resíduo: **remova-a** em vez de usar `--force`, para a issue não mentir sobre o estado |
|
|
91
|
+
| `pr-pending` | o documento está num PR aberto. Revise e mergeie — regerar descarta a revisão |
|
|
92
|
+
| `branch-without-pr` | o commit passou e o PR não nasceu. Rode `doctor`: quase sempre é a opção de organização ou o `GH_PR_TOKEN` |
|
|
93
|
+
| `stale-checkout` | o documento já está publicado na base e não no seu clone. `git pull` |
|
|
94
|
+
| `needs-confirmation` | o passo cria issues. Revise o rascunho e confirme com `--apply` |
|
|
95
|
+
| `inconsistent-state` | uma label afirma um documento que não existe — o título mudou depois de gerar? |
|
|
96
|
+
|
|
97
|
+
### O modelo
|
|
98
|
+
|
|
99
|
+
Para forçar um modelo num passo, aplique `spec-wave:model:<apelido>` — os apelidos válidos são os de `ai.modelAliases` no `.spec-wave.json`, e só esses. Um apelido que aponta para um modelo que o provider configurado não serve falha no passo, não na label.
|
|
100
|
+
|
|
101
|
+
Confirme no log que o override pegou: `modelo: ... (origem: label)`.
|
|
102
|
+
|
|
103
|
+
> **Uma lição que sobrevive à troca de provider:** quando um passo estoura o teto de turnos sem gerar nada, **troque o modelo antes de mexer no contexto**. Enxugar o `tech_context` em 14% não destravou nada, porque a entrada eram ~20k tokens; trocar o modelo entregou plano + crítica em minutos.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 1 · Gerar o plano
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @spec-wave/cli@latest run <n> --dry-run
|
|
111
|
+
npx @spec-wave/cli@latest run <n> --yes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
O `generate-plan` já critica o resultado ao final, no mesmo passo. O documento sai num PR.
|
|
115
|
+
|
|
116
|
+
Antes deste passo, garanta que `.github/config/tech_context.yml` está **commitado e na base** — o passo lê o arquivo do repositório, não do seu disco.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 2 · Tratar a crítica adversarial
|
|
121
|
+
|
|
122
|
+
**Leia `reference/critica.md` antes de corrigir qualquer coisa.** Ele traz o que a crítica erra, como distinguir falso positivo de defeito real, e quando parar de iterar — é a parte desta skill que mais economiza tempo.
|
|
123
|
+
|
|
124
|
+
O essencial:
|
|
125
|
+
|
|
126
|
+
- **Confira o finding contra o arquivo antes de corrigir.** Cite o trecho que confirma ou refuta. É o que distingue "corrigi o que a IA mandou" de "verifiquei e o problema existe".
|
|
127
|
+
- **Reaplique a crítica depois de editar**, mesmo com veredito `limpa` — ela é não-determinística, e a segunda passada costuma pegar a correção incompleta.
|
|
128
|
+
- **Corrigir o documento à mão não é desvio** — é o que o fluxo pede quando a crítica reprova. Edite **no PR**, onde as edições são preservadas.
|
|
129
|
+
|
|
130
|
+
Para recriticar sem regerar:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx @spec-wave/cli@latest run <n> --yes --step critique
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**`--step critique`, nunca `--step plan`.** O segundo regera o plano do zero e joga fora a correção — é o erro que faz o ciclo não convergir.
|
|
137
|
+
|
|
138
|
+
Se aparecer `spec-wave:needs-human`, a crítica esgotou as tentativas. Pare e envolva o usuário — a label precisa sair à mão.
|
|
139
|
+
|
|
140
|
+
**Mergeie o PR do plano antes de seguir.** O `validate` lê da base.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 3 · Validar
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx @spec-wave/cli@latest run <n> --yes # validate
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Roda em segundos e **não usa IA**. Confere as seções obrigatórias de `spec.md` e `plan.md` e aplica `spec-wave:plan-approved`.
|
|
151
|
+
|
|
152
|
+
### O validate NÃO move a etapa
|
|
153
|
+
|
|
154
|
+
Primeiro ponto de parada silenciosa: a Feature passa na validação e **continua na etapa em que estava** — invisível na fila do TL, que lê `✅ Ready`. Nada no fluxo a move; o selo `plan-approved` é uma label, não uma coluna.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npx @spec-wave/cli@latest move <n> "ready"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Confirme depois lendo o board — a etapa nunca retrocede, então é melhor ver o estado do que supor.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 4 · Decompor — o rascunho
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx @spec-wave/cli@latest run <n> --yes # decompose
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Gera `docs/features/<slug>/decomposition.md` e o critica. **Nenhuma issue é criada nesta etapa** — é de propósito, para haver um artefato revisável antes do irreversível.
|
|
171
|
+
|
|
172
|
+
**Leia o rascunho e revise de verdade.** A crítica olha contradição com spec/plan; ela **não olha coerência do grafo de dependências**. O que vale conferir:
|
|
173
|
+
|
|
174
|
+
- as linhas `**Depende de:**` refletem o que cada Story realmente precisa;
|
|
175
|
+
- **nenhuma task de teste depende de coisa que a Story dela não declara** — já apareceu uma task pedindo E2E numa Story que declarava depender só da fundação; pelo grafo, ela rodaria antes de o backend existir e falharia;
|
|
176
|
+
- identificadores com a grafia do domínio e sem acento, se essa for a convenção do repositório;
|
|
177
|
+
- **nada contradiz as correções que você fez no `plan.md`** — isso precisa ser verificado, não presumido.
|
|
178
|
+
|
|
179
|
+
Ao apresentar, mostre o grafo de dependências e a contagem de Stories/Tasks — é o que permite ao usuário julgar em segundos.
|
|
180
|
+
|
|
181
|
+
Para corrigir, edite o `decomposition.md` **no PR** e reaplique `--step decompose`: o arquivo é criticado **como está**, não regerado. Recritique depois de editar mesmo que a passada anterior tenha vindo limpa.
|
|
182
|
+
|
|
183
|
+
Se reprovar aqui, a superfície a corrigir é o **`decomposition.md`** — os achados citam `Story N` / `Task N.M`, que são títulos daquele arquivo. Confundir com o `plan.md` trava o ciclo.
|
|
184
|
+
|
|
185
|
+
**Mas verifique se o defeito nasce no plano.** Já aconteceu de um achado apontar a `Task 4.1` com a causa no `plan.md`. Corrigir só o rascunho deixaria os dois divergentes, e a próxima geração traria o defeito de volta. Quando o achado descrever uma decisão técnica (e não um recorte de escopo), corrija nos dois.
|
|
186
|
+
|
|
187
|
+
**Pare aqui e peça aprovação**, salvo instrução explícita em contrário. E mergeie o PR do rascunho: o apply lê da base.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 5 · Aplicar
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx @spec-wave/cli@latest run <n> --yes --apply
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Isto é a aprovação humana** — não há nova crítica depois. É o passo mais caro de reverter: cria dezenas de issues, e desfazer exige fechar todas e remover `spec-wave:decomposed` à mão.
|
|
198
|
+
|
|
199
|
+
### Sempre rode o detector depois
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx @spec-wave/cli@latest order <n>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Se aparecer **`Etapa: —`** nas Stories, o apply falhou no board **e saiu com sucesso** — o passo fica verde. As issues nascem sem etapa nenhuma, que é pior que Backlog: invisíveis em toda tela que filtra por etapa. A causa conhecida é o `GH_PROJECT_TOKEN` sem permissão de Projects na organização; o log mostra `Could not resolve to a node with the global id of 'PVT_...'`.
|
|
206
|
+
|
|
207
|
+
O `order` só enxerga **Stories**. Confira as **Tasks por amostragem**, nas duas pontas da faixa de numeração criada.
|
|
208
|
+
|
|
209
|
+
**Reparo**, uma issue por vez (cobre Etapa + Status, não o Work Item Type):
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx @spec-wave/cli@latest move <numero> "ready"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Relatório final
|
|
218
|
+
|
|
219
|
+
Termine com o estado real, não com "concluído". O usuário precisa saber se pode mandar alguém pegar a Feature:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
## <Feature> (#N) — <etapa atual>
|
|
223
|
+
|
|
224
|
+
<grafo de dependências das Stories, com os números criados>
|
|
225
|
+
|
|
226
|
+
| | |
|
|
227
|
+
|---|---|
|
|
228
|
+
| Documentos | spec.md · plan.md · decomposition.md |
|
|
229
|
+
| Sub-issues | N Stories + M Tasks, todas em <etapa> |
|
|
230
|
+
| PRs | <os PRs de documento, e se foram mergeados> |
|
|
231
|
+
|
|
232
|
+
### Correções aplicadas
|
|
233
|
+
<o que você mudou no plan.md ou no decomposition.md, e por quê>
|
|
234
|
+
|
|
235
|
+
### O que ainda bloqueia a implementação
|
|
236
|
+
<ver abaixo>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Verifique antes de dizer "pronta para o dev"
|
|
240
|
+
|
|
241
|
+
Três coisas que o fluxo não checa e que já impediram a implementação de começar:
|
|
242
|
+
|
|
243
|
+
1. **`specKit.command` configurado** no `.spec-wave.json` (ou a env `SPEC_WAVE_IMPLEMENT_CMD`). Sem isso o `implement` monta o contexto e para, sem acionar agente nenhum — o agente lê `.spec-wave/implement-<n>.md` e implementa direto. O `spec-wave dev-agent` **exige** essa configuração. O `doctor` reporta.
|
|
244
|
+
2. **As dependências externas existem.** O `order` só enxerga as Stories filhas — ele não sabe que a spec declarou *outra Feature* como bloqueante. Leia a seção Dependências da `spec.md` e cheque se aquelas Features têm ao menos spec. Já aconteceu de duas bloqueantes não terem nem spec: as Stories de UI escreviam em arquivos de um wizard inexistente, enquanto as de backend podiam começar. Diga quais Stories dão para pegar hoje e quais não.
|
|
245
|
+
3. **O `GH_PROJECT_TOKEN` funciona**, senão `code-review.yml` e `qa.yml` também vão falhar em mover cards durante a implementação, provavelmente em silêncio. O `doctor` verifica a **presença** do secret, nunca a validade — um token presente e sem permissão passa despercebido.
|