@spec-wave/cli 0.26.0 → 0.27.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 +25 -0
- package/src/commands/decompose.mjs +166 -39
- package/src/commands/doctor.mjs +190 -3
- package/src/commands/generate-bug.mjs +22 -16
- package/src/commands/generate-plan.mjs +72 -23
- package/src/commands/generate-spec.mjs +19 -15
- package/src/commands/implement.mjs +40 -20
- package/src/commands/run.mjs +51 -30
- package/src/commands/validate.mjs +84 -17
- package/src/config.mjs +18 -0
- package/src/lib/artifact-pr.mjs +272 -0
- package/src/lib/artifact-publish.mjs +169 -0
- package/src/lib/doc-availability.mjs +23 -1
- package/src/lib/doc-source.mjs +162 -0
- package/src/lib/flow-run.mjs +9 -218
- package/src/lib/next-step.mjs +27 -4
- package/src/lib/pr-branch.mjs +10 -0
- package/src/lib/repo-links.mjs +8 -2
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/bug/SKILL.md +2 -2
- package/src/plugin/skills/decompose/SKILL.md +4 -4
- package/src/plugin/skills/plan/SKILL.md +1 -1
- package/src/plugin/skills/run/SKILL.md +3 -1
- package/src/plugin/skills/spec/SKILL.md +4 -4
- package/src/plugin/skills/workflow/SKILL.md +2 -2
- package/src/templates/skill/SKILL.md +7 -7
- package/src/templates/workflows/code-review.yml +13 -2
- package/src/templates/workflows/critique.yml +1 -1
- package/src/templates/workflows/decompose.yml +13 -2
- package/src/templates/workflows/generate-bug.yml +17 -6
- package/src/templates/workflows/generate-plan.yml +20 -7
- package/src/templates/workflows/generate-spec.yml +20 -7
- package/src/templates/workflows/qa.yml +13 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// De ONDE vem o conteúdo de um documento do fluxo.
|
|
2
|
+
//
|
|
3
|
+
// Enquanto os artefatos eram commitados na branch default, "o documento existe"
|
|
4
|
+
// e "o arquivo está no checkout" eram a mesma pergunta, e todo mundo respondia
|
|
5
|
+
// com `existsSync`. Com a publicação por Pull Request deixam de ser: um
|
|
6
|
+
// documento recém-gerado vive numa branch que ninguém mergeou ainda.
|
|
7
|
+
//
|
|
8
|
+
// Ignorar essa diferença não dá erro — dá coisa pior, em silêncio:
|
|
9
|
+
//
|
|
10
|
+
// • o `decompose` REGENERA um rascunho que um humano já editou (a invariante
|
|
11
|
+
// "um rascunho existente nunca é regenerado" é literalmente um `existsSync`);
|
|
12
|
+
// • o `validate` REPROVA por arquivo ausente e consome a label de gatilho;
|
|
13
|
+
// • o `generate-plan` gera o plano com o placeholder no lugar da spec.
|
|
14
|
+
//
|
|
15
|
+
// Por isso a resolução é uma só, em camadas, e todo leitor passa por aqui.
|
|
16
|
+
//
|
|
17
|
+
// I/O com dependências INJETADAS: é o que permite testar as quatro camadas sem
|
|
18
|
+
// mock de HTTP, que a suíte não tem.
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { getFileContent, findOpenPR } from '../api/github-rest.mjs';
|
|
24
|
+
import { artifactBranch } from './artifact-pr.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {'local'|'remote'|'pending-pr'|'branch-only'|'missing'|'unknown'} DocState
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Estados em que o documento EXISTE mas ainda não chegou à branch base (PURA).
|
|
32
|
+
*
|
|
33
|
+
* Os dois significam a mesma coisa para quem depende do documento — o passo
|
|
34
|
+
* seguinte não pode rodar — e coisas diferentes para quem precisa destravar:
|
|
35
|
+
* `pending-pr` espera revisão e merge; `branch-only` espera alguém abrir o PR.
|
|
36
|
+
*
|
|
37
|
+
* Existe como predicado, e não como comparação solta, porque a lista é
|
|
38
|
+
* consultada em seis lugares (validate, generate-plan, decompose, apply,
|
|
39
|
+
* next-step, run) e um deles esquecido é uma porta aberta: foi assim que o
|
|
40
|
+
* `--apply` quase criou issues a partir de um rascunho que ninguém revisou.
|
|
41
|
+
*
|
|
42
|
+
* @param {DocState} state
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isAwaitingMerge(state) {
|
|
46
|
+
return state === 'pending-pr' || state === 'branch-only';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Onde está o documento, e o seu conteúdo (I/O — NUNCA lança).
|
|
51
|
+
*
|
|
52
|
+
* A ordem das camadas é deliberada:
|
|
53
|
+
*
|
|
54
|
+
* 1. **disco** (`local`) — o que o usuário tem à mão vence; é o que ele está
|
|
55
|
+
* editando, e é também o caso do runner logo após o checkout.
|
|
56
|
+
* 2. **branch base** (`remote`) — mantém exatamente o significado que o G6 do
|
|
57
|
+
* next-step já dava a este estado: publicado, mas não no seu clone.
|
|
58
|
+
* 3. **branch do artefato** — e só então se há PR: com PR aberto, `pending-pr`;
|
|
59
|
+
* sem PR, `branch-only`. A ordem é essa porque perguntar pelo PR primeiro
|
|
60
|
+
* tornava invisível o documento cujo commit passou e cujo PR falhou (403 de
|
|
61
|
+
* organização, GH_PR_TOKEN errado): ele era reportado como inexistente, e a
|
|
62
|
+
* execução seguinte REGERAVA, pagando a IA de novo. Consultar a branch antes
|
|
63
|
+
* não custa chamada nenhuma — são duas requisições nos dois desfechos.
|
|
64
|
+
* 4. nada (`missing`); falha de rede em qualquer consulta → `unknown`.
|
|
65
|
+
*
|
|
66
|
+
* `unknown` nunca vira bloqueio: ficar offline não pode travar o fluxo. Quem
|
|
67
|
+
* decide o que fazer com cada estado é o chamador.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} params
|
|
70
|
+
* @param {string} params.token
|
|
71
|
+
* @param {string} params.owner
|
|
72
|
+
* @param {string} params.repo
|
|
73
|
+
* @param {string|null} params.root raiz do repositório (para o caminho absoluto)
|
|
74
|
+
* @param {string} params.pathRel caminho do documento no repositório
|
|
75
|
+
* @param {string} params.doc chave de ARTIFACT_DOCS
|
|
76
|
+
* @param {number|string} params.issueNumber
|
|
77
|
+
* @param {string|null} [params.base] branch base (null = deixa a API escolher)
|
|
78
|
+
* @param {object} [params.deps] injeção para teste
|
|
79
|
+
* @returns {Promise<{content: string|null, state: DocState, ref: string|null,
|
|
80
|
+
* pr: {number: number, url: string}|null}>}
|
|
81
|
+
*/
|
|
82
|
+
export async function loadArtifact({
|
|
83
|
+
token, owner, repo, root, pathRel, doc, issueNumber, base = null, deps = {},
|
|
84
|
+
}) {
|
|
85
|
+
const api = { getFileContent, findOpenPR, ...deps };
|
|
86
|
+
|
|
87
|
+
const abs = path.resolve(root || process.cwd(), pathRel);
|
|
88
|
+
if (existsSync(abs)) {
|
|
89
|
+
return { content: readFileSync(abs, 'utf-8'), state: 'local', ref: null, pr: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Sem token não há como consultar nada — mas "não consegui olhar" não é "não
|
|
93
|
+
// existe": afirmar `missing` aqui faria o chamador regerar por cima.
|
|
94
|
+
if (!token || !owner || !repo) {
|
|
95
|
+
return { content: null, state: 'unknown', ref: null, pr: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let naBase;
|
|
99
|
+
try {
|
|
100
|
+
naBase = await api.getFileContent(token, owner, repo, pathRel, base || undefined);
|
|
101
|
+
} catch {
|
|
102
|
+
return { content: null, state: 'unknown', ref: null, pr: null };
|
|
103
|
+
}
|
|
104
|
+
if (naBase != null) {
|
|
105
|
+
return { content: naBase, state: 'remote', ref: base, pr: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let branch;
|
|
109
|
+
try {
|
|
110
|
+
branch = artifactBranch({ issueNumber, doc });
|
|
111
|
+
} catch {
|
|
112
|
+
// Documento sem branch de artefato (ou número inválido): não há onde olhar.
|
|
113
|
+
return { content: null, state: 'missing', ref: null, pr: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let naBranch;
|
|
117
|
+
try {
|
|
118
|
+
naBranch = await api.getFileContent(token, owner, repo, pathRel, branch);
|
|
119
|
+
} catch {
|
|
120
|
+
return { content: null, state: 'unknown', ref: null, pr: null };
|
|
121
|
+
}
|
|
122
|
+
// Branch inexistente e arquivo ausente dão o mesmo 404, e é o mesmo desfecho.
|
|
123
|
+
if (naBranch == null) return { content: null, state: 'missing', ref: null, pr: null };
|
|
124
|
+
|
|
125
|
+
// O PR só importa depois de saber que o documento está lá — e o que ele decide
|
|
126
|
+
// é o REMÉDIO, não a existência.
|
|
127
|
+
let pr;
|
|
128
|
+
try {
|
|
129
|
+
pr = await api.findOpenPR(token, owner, repo, branch);
|
|
130
|
+
} catch {
|
|
131
|
+
// O documento existe; não saber se há PR não pode rebaixá-lo a inexistente,
|
|
132
|
+
// que é o estado que autoriza regerar por cima.
|
|
133
|
+
return { content: naBranch, state: 'branch-only', ref: branch, pr: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return pr
|
|
137
|
+
? { content: naBranch, state: 'pending-pr', ref: branch, pr: { number: pr.number, url: pr.url } }
|
|
138
|
+
: { content: naBranch, state: 'branch-only', ref: branch, pr: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Estado de VÁRIOS documentos de uma issue (I/O — nunca lança).
|
|
143
|
+
*
|
|
144
|
+
* Devolve o mapa que `nextStep` consome, sem o conteúdo: quem decide o próximo
|
|
145
|
+
* passo não precisa carregar três documentos inteiros na memória.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} params
|
|
148
|
+
* @param {Array<{doc: string, pathRel: string}>} params.docs
|
|
149
|
+
* @returns {Promise<Record<string, {state: DocState, pr: object|null}>>}
|
|
150
|
+
*/
|
|
151
|
+
export async function artifactStates({
|
|
152
|
+
token, owner, repo, root, docs = [], issueNumber, base = null, deps = {},
|
|
153
|
+
}) {
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const { doc, pathRel } of docs) {
|
|
156
|
+
const { state, pr } = await loadArtifact({
|
|
157
|
+
token, owner, repo, root, pathRel, doc, issueNumber, base, deps,
|
|
158
|
+
});
|
|
159
|
+
out[doc] = { state, pr };
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
package/src/lib/flow-run.mjs
CHANGED
|
@@ -9,26 +9,17 @@
|
|
|
9
9
|
// O modo é detectado pelo ambiente (`GITHUB_ACTIONS`), não por flag: um
|
|
10
10
|
// comando com dois nomes para a mesma coisa envelhece mal.
|
|
11
11
|
//
|
|
12
|
-
// **O comportamento é
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// local que não sincronizasse o board deixaria o próximo passo do fluxo cego.
|
|
12
|
+
// **O comportamento é IDÊNTICO nos dois modos** — gera, publica em Pull Request,
|
|
13
|
+
// comenta na issue e avança a Etapa. O board é a fonte de verdade do RFC-001
|
|
14
|
+
// independentemente de onde a geração rodou.
|
|
16
15
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// 2. FALHA DE PUSH. No Action, não conseguir publicar é falha do job. Local, o
|
|
24
|
-
// arquivo já está gerado e commitado — perder isso porque o remoto andou
|
|
25
|
-
// seria pior que avisar e deixar você resolver o push. Antes de chegar
|
|
26
|
-
// nessa bifurcação, os dois modos repetem pull+push enquanto a rejeição for
|
|
27
|
-
// disputa com outro run — ver §publicação concorrente.
|
|
16
|
+
// Este módulo já carregou as DUAS exceções que sobravam dessa promessa
|
|
17
|
+
// (identidade do git e falha de push), junto com o commit direto na branch
|
|
18
|
+
// default, o laço de pull+rebase+push e a classificação de corrida. Nada disso
|
|
19
|
+
// existe mais: a publicação passou a ser via API, em branch própria por
|
|
20
|
+
// documento, e mora em lib/artifact-publish.mjs. As duas exceções eram
|
|
21
|
+
// consequência de mexer em git local — some a causa, somem elas.
|
|
28
22
|
|
|
29
|
-
import { execSync } from 'node:child_process';
|
|
30
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
31
|
-
import path from 'node:path';
|
|
32
23
|
import { resolveRepoContext } from './project-root.mjs';
|
|
33
24
|
import { CONFIG_FILE } from '../config.mjs';
|
|
34
25
|
|
|
@@ -72,203 +63,3 @@ export function resolveFlowContext({ cwd = process.cwd(), command = 'este comand
|
|
|
72
63
|
}
|
|
73
64
|
return { owner, repo, root, config, mode };
|
|
74
65
|
}
|
|
75
|
-
|
|
76
|
-
// --- publicação concorrente -------------------------------------------------
|
|
77
|
-
//
|
|
78
|
-
// `pull --rebase` seguido de `push` NÃO é atômico. Entre os dois cabe o push de
|
|
79
|
-
// outro run: dois `generate-spec` de issues DIFERENTES disparados juntos rodam
|
|
80
|
-
// em paralelo (a `concurrency` dos workflows é por issue, e é por issue que ela
|
|
81
|
-
// tem que ser — serializar o repositório inteiro mataria a vazão), terminam
|
|
82
|
-
// juntos e disputam a ponta da branch. O perdedor levava non-fast-forward,
|
|
83
|
-
// falhava o job e PERDIA o documento recém-gerado — com a chamada de IA já
|
|
84
|
-
// paga. Repetir pull+push resolve: o rebase reaplica o commit sobre a ponta
|
|
85
|
-
// nova e o segundo push passa.
|
|
86
|
-
|
|
87
|
-
export const PUSH_ATTEMPTS = 5;
|
|
88
|
-
const PUSH_BASE_MS = 500;
|
|
89
|
-
|
|
90
|
-
// Rejeição por CORRIDA: o remoto andou, e reaplicar por cima resolve.
|
|
91
|
-
const RACE_PATTERNS = /non-fast-forward|fetch first|Updates were rejected|cannot lock ref|failed to lock/i;
|
|
92
|
-
|
|
93
|
-
// Recusa DELIBERADA do remoto. Compartilha a linha genérica "failed to push
|
|
94
|
-
// some refs" com a corrida, então precisa ser testada ANTES: repetir aqui só
|
|
95
|
-
// atrasa a mensagem que o usuário precisa ler. Mesma filosofia de
|
|
96
|
-
// `isTransientProviderError` (lib/claude.mjs).
|
|
97
|
-
const REFUSED_PATTERNS = /GH006|protected branch|pre-receive hook declined|permission to .* denied|403 Forbidden|Authentication failed|could not read Username|Repository not found/i;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* A saída do git indica disputa pela ponta da branch? (função PURA)
|
|
101
|
-
*
|
|
102
|
-
* @param {string} output stdout+stderr do comando que falhou
|
|
103
|
-
* @returns {boolean}
|
|
104
|
-
*/
|
|
105
|
-
export function isRacePushError(output) {
|
|
106
|
-
const text = String(output || '');
|
|
107
|
-
if (!text) return false;
|
|
108
|
-
if (REFUSED_PATTERNS.test(text)) return false;
|
|
109
|
-
return RACE_PATTERNS.test(text);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Espera antes da próxima tentativa (função PURA).
|
|
114
|
-
*
|
|
115
|
-
* Exponencial COM jitter: sem o jitter, dois runs que colidiram uma vez voltam
|
|
116
|
-
* a colidir no mesmo instante — o backoff determinístico os mantém em fase.
|
|
117
|
-
*
|
|
118
|
-
* @param {number} attempt tentativa que acabou de falhar (1-based)
|
|
119
|
-
* @param {object} [opts]
|
|
120
|
-
* @param {number} [opts.baseMs]
|
|
121
|
-
* @param {() => number} [opts.random] injetável para o teste
|
|
122
|
-
* @returns {number} milissegundos
|
|
123
|
-
*/
|
|
124
|
-
export function pushBackoffMs(attempt, { baseMs = PUSH_BASE_MS, random = Math.random } = {}) {
|
|
125
|
-
const teto = baseMs * 2 ** (attempt - 1); // 500ms, 1s, 2s, 4s…
|
|
126
|
-
return Math.round(teto * (1 + random())); // [teto, 2*teto)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Sleep BLOQUEANTE de propósito: `commitGenerated` é síncrona de ponta a ponta
|
|
130
|
-
// (execSync), e um único `await` no meio obrigaria os três comandos chamadores
|
|
131
|
-
// e a suíte inteira a virarem async para nada — nada mais roda nesta thread
|
|
132
|
-
// enquanto o git trabalha.
|
|
133
|
-
function sleepSync(ms) {
|
|
134
|
-
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Roda um comando git capturando a saída, e a devolve anexada ao erro.
|
|
139
|
-
*
|
|
140
|
-
* Com `stdio: 'inherit'` o texto do git vai para o log mas NÃO chega ao
|
|
141
|
-
* processo: `err.message` é só "Command failed: git push", e não dá para
|
|
142
|
-
* distinguir corrida de branch protegida. Capturando, o log continua igual
|
|
143
|
-
* (reemitimos) e a classificação passa a ser possível.
|
|
144
|
-
*/
|
|
145
|
-
function gitCaptured(cmd) {
|
|
146
|
-
try {
|
|
147
|
-
const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
148
|
-
if (out?.length) process.stdout.write(out);
|
|
149
|
-
return;
|
|
150
|
-
} catch (err) {
|
|
151
|
-
const detail = [err.stdout, err.stderr].map(b => b?.toString() || '').join('').trim();
|
|
152
|
-
if (detail) process.stderr.write(`${detail}\n`);
|
|
153
|
-
const resumo = detail.split('\n').find(l => l.trim()) || err.message;
|
|
154
|
-
const erro = new Error(`${cmd} falhou: ${resumo}`);
|
|
155
|
-
erro.gitOutput = detail;
|
|
156
|
-
throw erro;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Rebase interrompido (conflito, ou pull abortado no meio) deixa o repositório
|
|
161
|
-
// EM rebase. No runner descartável tanto faz; no clone do usuário, sair assim
|
|
162
|
-
// é deixar um estado que ele não pediu e talvez nem perceba.
|
|
163
|
-
function abortRebaseIfAny() {
|
|
164
|
-
try {
|
|
165
|
-
const dir = execSync('git rev-parse --git-path rebase-merge', { stdio: 'pipe' }).toString().trim();
|
|
166
|
-
const dirApply = execSync('git rev-parse --git-path rebase-apply', { stdio: 'pipe' }).toString().trim();
|
|
167
|
-
if (!existsSync(dir) && !existsSync(dirApply)) return;
|
|
168
|
-
execSync('git rebase --abort', { stdio: 'pipe' });
|
|
169
|
-
} catch {
|
|
170
|
-
// Melhor esforço: se nem abortar dá, o erro original é o que importa.
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* pull --rebase + push, repetindo enquanto a falha for disputa pela branch.
|
|
176
|
-
*
|
|
177
|
-
* @returns {{attempts: number}}
|
|
178
|
-
* @throws o erro do git quando não é corrida, ou quando as tentativas acabam
|
|
179
|
-
*/
|
|
180
|
-
function publishWithRetry({ attempts, baseMs }) {
|
|
181
|
-
for (let attempt = 1; ; attempt++) {
|
|
182
|
-
try {
|
|
183
|
-
gitCaptured('git pull --rebase');
|
|
184
|
-
gitCaptured('git push');
|
|
185
|
-
return { attempts: attempt };
|
|
186
|
-
} catch (err) {
|
|
187
|
-
abortRebaseIfAny();
|
|
188
|
-
if (attempt >= attempts || !isRacePushError(err.gitOutput || err.message)) throw err;
|
|
189
|
-
const espera = pushBackoffMs(attempt, { baseMs });
|
|
190
|
-
console.warn(
|
|
191
|
-
`Publicação disputada por outro run (tentativa ${attempt}/${attempts}) — ` +
|
|
192
|
-
`repetindo em ${(espera / 1000).toFixed(1)}s.`
|
|
193
|
-
);
|
|
194
|
-
sleepSync(espera);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Grava um arquivo gerado e o publica: commit + pull --rebase + push.
|
|
201
|
-
*
|
|
202
|
-
* Era o mesmo bloco copiado em `generate-spec`, `generate-plan` e `decompose`,
|
|
203
|
-
* com a identidade do bot embutida. Centralizado aqui para que a diferença
|
|
204
|
-
* entre os modos exista num lugar só.
|
|
205
|
-
*
|
|
206
|
-
* O commit é escopado ao caminho (`git commit -- <arquivo>`): sem isso, qualquer
|
|
207
|
-
* coisa que você já tivesse no index entraria junto no commit do spec-wave —
|
|
208
|
-
* irrelevante num runner limpo, nada irrelevante no seu clone.
|
|
209
|
-
*
|
|
210
|
-
* @param {object} params
|
|
211
|
-
* @param {string} params.filePath caminho absoluto do arquivo
|
|
212
|
-
* @param {string} params.content
|
|
213
|
-
* @param {string} params.message mensagem de commit
|
|
214
|
-
* @param {'actions'|'local'} params.mode
|
|
215
|
-
* @param {{attempts?: number, baseMs?: number}} [params.retry] ajuste do laço de
|
|
216
|
-
* publicação — existe para o teste não dormir o backoff real
|
|
217
|
-
* @returns {{committed: boolean, pushed: boolean, warning: string|null}}
|
|
218
|
-
*/
|
|
219
|
-
export function commitGenerated({ filePath, content, message, mode, retry = {} }) {
|
|
220
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
221
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
222
|
-
|
|
223
|
-
const git = (cmd, opts = {}) => execSync(cmd, { stdio: 'inherit', ...opts });
|
|
224
|
-
const gitQuiet = (cmd) => execSync(cmd, { stdio: 'pipe' }).toString().trim();
|
|
225
|
-
|
|
226
|
-
if (mode === 'actions') {
|
|
227
|
-
// Runner descartável: identidade do bot é o que se quer no histórico.
|
|
228
|
-
git('git config user.email "spec-wave[bot]@github.com"');
|
|
229
|
-
git('git config user.name "spec-wave[bot]"');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
git(`git add "${filePath}"`);
|
|
233
|
-
|
|
234
|
-
// Nada mudou (regerar conteúdo idêntico) → `git commit` sairia 1 e derrubaria
|
|
235
|
-
// o comando depois de o trabalho estar feito.
|
|
236
|
-
let hasChanges = true;
|
|
237
|
-
try {
|
|
238
|
-
execSync(`git diff --cached --quiet -- "${filePath}"`, { stdio: 'pipe' });
|
|
239
|
-
hasChanges = false;
|
|
240
|
-
} catch {
|
|
241
|
-
hasChanges = true;
|
|
242
|
-
}
|
|
243
|
-
if (!hasChanges) {
|
|
244
|
-
return { committed: false, pushed: false, warning: 'conteúdo idêntico ao já versionado — nada a commitar' };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
git(`git commit -m "${message}" -- "${filePath}"`);
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
publishWithRetry({
|
|
251
|
-
attempts: retry.attempts ?? PUSH_ATTEMPTS,
|
|
252
|
-
baseMs: retry.baseMs ?? PUSH_BASE_MS,
|
|
253
|
-
});
|
|
254
|
-
return { committed: true, pushed: true, warning: null };
|
|
255
|
-
} catch (err) {
|
|
256
|
-
if (mode === 'actions') throw err;
|
|
257
|
-
// Local: o arquivo está gerado e commitado. Derrubar o comando aqui
|
|
258
|
-
// esconderia esse fato atrás de um erro de rede/divergência.
|
|
259
|
-
const branch = (() => {
|
|
260
|
-
try {
|
|
261
|
-
return gitQuiet('git rev-parse --abbrev-ref HEAD');
|
|
262
|
-
} catch {
|
|
263
|
-
return 'seu branch';
|
|
264
|
-
}
|
|
265
|
-
})();
|
|
266
|
-
return {
|
|
267
|
-
committed: true,
|
|
268
|
-
pushed: false,
|
|
269
|
-
warning:
|
|
270
|
-
`commit feito em ${branch}, mas o push falhou (${err.message.split('\n')[0]}). ` +
|
|
271
|
-
'O arquivo está salvo e versionado — publique quando resolver.',
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
}
|
package/src/lib/next-step.mjs
CHANGED
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_PLAN_APPROVED, LABEL_TRIAGED,
|
|
20
20
|
LABEL_DUPLICATE, LABEL_WONT_FIX, LABEL_READY, labelNames,
|
|
21
21
|
} from '../config.mjs';
|
|
22
|
+
import { awaitingMergeBlock } from './artifact-pr.mjs';
|
|
23
|
+
import { isAwaitingMerge } from './doc-source.mjs';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
* @typedef {'local'|'remote'|'missing'|'unknown'} DocState
|
|
26
|
+
* @typedef {'local'|'remote'|'pending-pr'|'missing'|'unknown'} DocState
|
|
25
27
|
* @typedef {'generate-spec'|'generate-plan'|'critique'|'validate'|'decompose'
|
|
26
28
|
* |'decompose-apply'|'generate-bug'|'triage'|'implement'|'none'} StepAction
|
|
27
29
|
*/
|
|
@@ -211,6 +213,8 @@ export function resolveForcedStep(step, { type }) {
|
|
|
211
213
|
* @param {'open'|'closed'} [params.state]
|
|
212
214
|
* @param {Array<string|{name:string}>} [params.labels]
|
|
213
215
|
* @param {Record<string, DocState>} [params.docs] estado de spec/plan/decomposition/bug
|
|
216
|
+
* @param {Record<string, object>} [params.docPrs] PR aberto de cada documento em
|
|
217
|
+
* `pending-pr` — só para a mensagem
|
|
214
218
|
* @param {Record<string, string>} [params.docPaths] caminhos relativos (só para as mensagens)
|
|
215
219
|
* @param {StepAction|null} [params.forcedStep] resultado de resolveForcedStep
|
|
216
220
|
* @param {boolean} [params.confirmed] `--yes`/`--apply` já dados
|
|
@@ -220,7 +224,7 @@ export function resolveForcedStep(step, { type }) {
|
|
|
220
224
|
* blocked: null|{code: string, message: string, unblock: string}}}
|
|
221
225
|
*/
|
|
222
226
|
export function nextStep({
|
|
223
|
-
type, issueNumber, state = 'open', labels = [], docs = {}, docPaths = {},
|
|
227
|
+
type, issueNumber, state = 'open', labels = [], docs = {}, docPaths = {}, docPrs = {},
|
|
224
228
|
forcedStep = null, confirmed = false, boardReady = true, force = false,
|
|
225
229
|
} = {}) {
|
|
226
230
|
const params = { issueNumber };
|
|
@@ -285,6 +289,25 @@ export function nextStep({
|
|
|
285
289
|
}
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
// G5.5 — documento gerado e esperando merge. Precisa vir ANTES do
|
|
293
|
+
// `pipelineStep`: para ele, um documento que não está na base "não existe", e
|
|
294
|
+
// a resposta seria REGERAR — pagando a chamada de IA outra vez, a cada
|
|
295
|
+
// execução, e descartando o que o revisor já editou dentro do PR.
|
|
296
|
+
//
|
|
297
|
+
// Antes do `forcedStep` também, de propósito: `--step spec --yes` é
|
|
298
|
+
// exatamente o comando que alguém impaciente rodaria aqui, e ele passaria por
|
|
299
|
+
// cima da revisão em curso.
|
|
300
|
+
for (const doc of docsOfType) {
|
|
301
|
+
const estado = docState(docs, doc);
|
|
302
|
+
if (isAwaitingMerge(estado)) {
|
|
303
|
+
const info = docPrs[doc] || {};
|
|
304
|
+
return decided('none', `\`${pathOf(doc)}\` foi gerado e ainda não está na base.`, params,
|
|
305
|
+
awaitingMergeBlock({
|
|
306
|
+
pathRel: pathOf(doc), state: estado, pr: info.pr || null, branch: info.branch || null,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
288
311
|
const action = forcedStep || pipelineStep({ type, docs, has });
|
|
289
312
|
if (action === 'none') {
|
|
290
313
|
return decided('none', nothingPendingReason({ type, has }), params);
|
|
@@ -299,8 +322,8 @@ export function nextStep({
|
|
|
299
322
|
`Rode \`${stepCommand(action, params)}\` quando decidir.`));
|
|
300
323
|
}
|
|
301
324
|
|
|
302
|
-
// G6 — documento que o passo lê ou escreve existe só
|
|
303
|
-
//
|
|
325
|
+
// G6 — documento que o passo lê ou escreve existe só na branch base. Gerar por
|
|
326
|
+
// cima publicaria um Pull Request que desfaz o documento bom.
|
|
304
327
|
for (const doc of stepDocs(action, type)) {
|
|
305
328
|
if (docState(docs, doc) === 'remote') {
|
|
306
329
|
return decided(action, `\`${pathOf(doc)}\` existe no repositório mas não no seu clone.`, params,
|
package/src/lib/pr-branch.mjs
CHANGED
|
@@ -251,6 +251,16 @@ export function explainGitWriteError(err) {
|
|
|
251
251
|
case 401:
|
|
252
252
|
return `${msg} — token inválido ou expirado.`;
|
|
253
253
|
case 403:
|
|
254
|
+
// O GitHub Actions recusa a criação de PR com uma mensagem própria, e ela
|
|
255
|
+
// é a falha MAIS provável deste fluxo: a opção "Allow GitHub Actions to
|
|
256
|
+
// create and approve pull requests" vem desligada em muitas organizações.
|
|
257
|
+
// Sem este ramo, o usuário lê "permissão de escrita" e vai mexer em
|
|
258
|
+
// escopo de token, que não é o problema.
|
|
259
|
+
if (/not permitted to create or approve pull requests/i.test(msg)) {
|
|
260
|
+
return `${msg} — o GITHUB_TOKEN está proibido de abrir Pull Requests neste ` +
|
|
261
|
+
'repositório. Ligue Settings → Actions → General → "Allow GitHub Actions to ' +
|
|
262
|
+
'create and approve pull requests", ou forneça um PAT no secret `GH_PR_TOKEN`.';
|
|
263
|
+
}
|
|
254
264
|
return `${msg} — o token não tem permissão de escrita. Precisa de "Contents: write" e ` +
|
|
255
265
|
'"Pull requests: write" (fine-grained) ou do escopo `repo` (classic).';
|
|
256
266
|
case 404:
|
package/src/lib/repo-links.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export function blobUrl({ owner, repo, ref, pathRel }) {
|
|
|
26
26
|
* Qual ref o link deve citar (função PURA).
|
|
27
27
|
*
|
|
28
28
|
* No Actions, `GITHUB_REF_NAME` é a branch do evento. Localmente, a branch
|
|
29
|
-
* corrente é a que
|
|
29
|
+
* corrente é a que o usuário está usando. `main` é o último
|
|
30
30
|
* recurso — o mesmo valor que estava hardcoded, então nunca piora.
|
|
31
31
|
*
|
|
32
32
|
* @param {object} params
|
|
@@ -71,9 +71,15 @@ export function currentGitBranch(cwd = process.cwd()) {
|
|
|
71
71
|
* @param {'actions'|'local'} params.mode
|
|
72
72
|
* @param {string|null} [params.root] raiz do repo (para ler a branch corrente)
|
|
73
73
|
* @param {object|null} [params.config]
|
|
74
|
+
* @param {string|null} [params.ref] ref EXPLÍCITA, com precedência sobre tudo.
|
|
75
|
+
* Com a publicação por Pull Request, quem gerou o documento sabe exatamente em
|
|
76
|
+
* que branch ele está — e a inferência abaixo erraria: num evento
|
|
77
|
+
* `issues: labeled` o `GITHUB_REF_NAME` é a branch DEFAULT, onde o documento
|
|
78
|
+
* ainda não existe, então todo link nasceria 404 até o merge.
|
|
74
79
|
* @returns {string}
|
|
75
80
|
*/
|
|
76
|
-
export function docBlobUrl({ owner, repo, pathRel, mode, root = null, config = null }) {
|
|
81
|
+
export function docBlobUrl({ owner, repo, pathRel, mode, root = null, config = null, ref: refExplicita = null }) {
|
|
82
|
+
if (refExplicita) return blobUrl({ owner, repo, ref: refExplicita, pathRel });
|
|
77
83
|
const ref = resolveDocRef({
|
|
78
84
|
mode,
|
|
79
85
|
env: process.env,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-wave-bug
|
|
3
|
-
description: "Use para gerar o bug.md de um defeito do spec-wave — reprodução, causa raiz, escopo do fix e teste de regressão. Aplica a label spec-wave:bug e deixa o GitHub Action gerar
|
|
3
|
+
description: "Use para gerar o bug.md de um defeito do spec-wave — reprodução, causa raiz, escopo do fix e teste de regressão. Aplica a label spec-wave:bug e deixa o GitHub Action gerar o arquivo em docs/bugs/<slug>/ e abrir um Pull Request. Gatilhos: 'gerar o bug.md da issue 42', 'documentar a causa raiz do bug', 'rodar o spec-wave:bug'. Só vale para issues do tipo Bug — Feature usa spec/plan."
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Bash(gh issue *)
|
|
6
6
|
- Bash(npx @spec-wave/cli@latest *)
|
|
@@ -9,7 +9,7 @@ allowed-tools:
|
|
|
9
9
|
|
|
10
10
|
# spec-wave bug — o documento do defeito
|
|
11
11
|
|
|
12
|
-
> **Regra fundamental: nunca escreva o `bug.md` você mesmo.** Aplique a label e deixe o Action gerar — é isso que garante que o arquivo
|
|
12
|
+
> **Regra fundamental: nunca escreva o `bug.md` você mesmo.** Aplique a label e deixe o Action gerar — é isso que garante que o arquivo chegue à main por Pull Request e seja referenciado na issue. Exceção: revisar/melhorar um `bug.md` já gerado (aí sim use Edit no arquivo local).
|
|
13
13
|
|
|
14
14
|
**Contexto:** leia `.spec-wave.json` (Read). Ausente → skill **setup**.
|
|
15
15
|
|
|
@@ -24,7 +24,7 @@ Para qualquer outro tipo (Spike, Bug, Story, Task…) o Action **recusa** e come
|
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
spec-wave:decompose
|
|
27
|
-
├─ decomposition.md ausente → gera via IA →
|
|
27
|
+
├─ decomposition.md ausente → gera via IA → abre Pull Request → critica
|
|
28
28
|
└─ decomposition.md presente → critica o arquivo COMO ESTÁ (preserva suas edições)
|
|
29
29
|
├─ grave → +critique-failed, comentário citando Story N / Task N.M, exit 1
|
|
30
30
|
└─ limpo → +decompose-ready, comentário "revise e aplique"
|
|
@@ -45,14 +45,14 @@ spec-wave:decompose-apply
|
|
|
45
45
|
# ou Action — assíncrono
|
|
46
46
|
gh issue edit <número> --add-label "spec-wave:decompose"
|
|
47
47
|
```
|
|
48
|
-
Informe: "Rascunho iniciado — vai
|
|
48
|
+
Informe: "Rascunho iniciado — vai abrir um Pull Request com o `decomposition.md` e passar pela crítica. **Nenhuma issue é criada nesta etapa.**"
|
|
49
49
|
|
|
50
50
|
3. **Leia o resultado** quando o Action terminar:
|
|
51
51
|
|
|
52
52
|
| Label resultante | O que fazer |
|
|
53
53
|
|------------------|-------------|
|
|
54
54
|
| `spec-wave:decompose-ready` | Passou na crítica. **Leia o `decomposition.md`** e mostre ao usuário o que será criado (Stories, Tasks, dependências). Ofereça editar antes de aplicar. |
|
|
55
|
-
| `spec-wave:critique-failed` | O Action **falhou (exit 1)** e nada foi criado. Os achados citam `Story N` / `Task N.M`. **Corrija o `decomposition.md`** — não o `plan.md` —
|
|
55
|
+
| `spec-wave:critique-failed` | O Action **falhou (exit 1)** e nada foi criado. Os achados citam `Story N` / `Task N.M`. **Corrija o `decomposition.md`** — não o `plan.md` — **no Pull Request** e reaplique `spec-wave:decompose` (o arquivo é criticado como está, sem ser regerado, venha ele do PR ou da base). |
|
|
56
56
|
| `spec-wave:needs-human` | A crítica esgotou as tentativas. **Pare** e envolva o usuário: as duas labels precisam sair à mão. |
|
|
57
57
|
|
|
58
58
|
4. **Etapa 2 — aplicar o rascunho aprovado**, só depois da revisão:
|
|
@@ -95,7 +95,7 @@ Corpo técnico.
|
|
|
95
95
|
- a **posição** manda, não o número escrito — inserir uma Story no meio sem renumerar funciona
|
|
96
96
|
- `**Depende de:**` aceita **irmãs** (`Story 1, Story 3`, 1-based, só para trás — apontar para si mesma ou para frente é **erro**, não filtro silencioso) e **issues de outras Features** (`#412`, que precisam JÁ existir); as duas formas convivem na mesma linha (`Story 1, #412`), e `—` significa nenhuma
|
|
97
97
|
- o corpo aceita markdown livre (`## Backend`, cercas de código) — só `## Story N` e `### Task N.M` são estrutura
|
|
98
|
-
- **para gerar outro rascunho do zero:** apague o arquivo e reaplique `spec-wave:decompose`
|
|
98
|
+
- **para gerar outro rascunho do zero:** feche o Pull Request e apague a branch `spec-wave/<n>-decompose` (ou apague o arquivo, se já foi mergeado) e reaplique `spec-wave:decompose`
|
|
99
99
|
|
|
100
100
|
> ⚠️ O slug vem do **título**. Renomear a Feature entre o rascunho e o apply muda o diretório e órfã o `decomposition.md` — o apply reclama que não achou o rascunho.
|
|
101
101
|
|
|
@@ -14,7 +14,7 @@ allowed-tools:
|
|
|
14
14
|
|
|
15
15
|
> **Regra fundamental: nunca escreva o `plan.md` à mão.** Quem gera é o spec-wave — pelo Action ou pela CLI local. Exceção: revisar/melhorar um plano já gerado.
|
|
16
16
|
|
|
17
|
-
**Dois modos, mesmo resultado.** `npx @spec-wave/cli@latest generate-plan --issue-number <n>` roda **agora**, nesta sessão; a label `spec-wave:plan` roda no Action. O modo é detectado pelo ambiente. Ambos geram,
|
|
17
|
+
**Dois modos, mesmo resultado.** `npx @spec-wave/cli@latest generate-plan --issue-number <n>` roda **agora**, nesta sessão; a label `spec-wave:plan` roda no Action. O modo é detectado pelo ambiente. Ambos geram, abrem um **Pull Request**, criticam e comentam na issue. Local exige a chave de IA no seu ambiente. Veja a skill **spec** para a tabela completa.
|
|
18
18
|
|
|
19
19
|
**Contexto:** leia `.spec-wave.json` (Read). Ausente → skill **setup**.
|
|
20
20
|
|
|
@@ -50,13 +50,15 @@ O comando **explica e para**, saindo com código 2. Não force sem entender:
|
|
|
50
50
|
- **`trigger-pending`** — há label de gatilho na issue: um Action está em voo (ou falhou deixando-a). Rodar por cima duplicaria documento e comentário. `--force` fura, quando você tem certeza.
|
|
51
51
|
- **`needs-human` / `critique-failed`** — portões humanos da crítica. O caminho é corrigir o documento e remover a label; para o `critique-failed` o passo de retomada é **re-criticar**, nunca regerar.
|
|
52
52
|
- **`stale-checkout`** — o documento existe no repositório e não no seu clone. `git pull` e repita; seguir sobrescreveria o que já foi publicado.
|
|
53
|
+
- **`pr-pending`** — o documento foi gerado e está num **Pull Request ainda não mergeado**. Revise o PR (edite ali mesmo, se precisar) e faça o merge; o passo seguinte lê o documento da branch base. Rodar por cima regeraria o arquivo, descartando a revisão e pagando a IA de novo.
|
|
54
|
+
- **`branch-without-pr`** — o documento foi commitado numa branch `spec-wave/*` e **nenhum PR foi aberto** para ela. Quase sempre a abertura do PR falhou: opção *"Allow GitHub Actions to create and approve pull requests"* desligada, ou secret `GH_PR_TOKEN` ausente. Rode `spec-wave doctor`, corrija e abra o PR — reaplicar o gatilho **regera** o documento.
|
|
53
55
|
- **`needs-confirmation`** — o passo cria issues (`decompose-apply`) ou gasta IA repetindo a crítica de um rascunho existente. Revise antes e confirme com `--apply` / `--yes`.
|
|
54
56
|
- **`inconsistent-state`** — as labels afirmam um documento que não existe. Quase sempre o título da issue mudou depois de gerar (o slug vira outro diretório).
|
|
55
57
|
|
|
56
58
|
## Regras que o `run` respeita — e você também
|
|
57
59
|
|
|
58
60
|
- **Nunca aplique label de gatilho no modo local.** Ela dispararia o Action e o passo aconteceria duas vezes. O `run` não aplica nenhuma.
|
|
59
|
-
- **Um passo por invocação.** `--max-steps <n>` encadeia, mas cada passo de IA custa dinheiro — encadeie só quando o usuário pedir.
|
|
61
|
+
- **Um passo por invocação.** `--max-steps <n>` encadeia, mas cada passo de IA custa dinheiro — encadeie só quando o usuário pedir. Na prática o encadeamento agora para sozinho: cada documento vai para um Pull Request, e o passo seguinte depende do merge.
|
|
60
62
|
- A **Regra fundamental** continua valendo: nunca escreva `spec.md`/`plan.md`/`decomposition.md` à mão. Quem gera é a CLI, aqui como no Action.
|
|
61
63
|
|
|
62
64
|
## `mode` — o interruptor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-wave-spec
|
|
3
|
-
description: "Use para iniciar a geração da especificação funcional (spec.md) de uma Feature do spec-wave — o PRIMEIRO documento do ciclo, antes do plano técnico. Aplica a label spec-wave:spec e deixa o GitHub Action gerar e
|
|
3
|
+
description: "Use para iniciar a geração da especificação funcional (spec.md) de uma Feature do spec-wave — o PRIMEIRO documento do ciclo, antes do plano técnico. Aplica a label spec-wave:spec e deixa o GitHub Action gerar o arquivo e abrir um Pull Request. Gatilhos: 'gerar a spec da feature 12', 'criar especificação funcional', 'rodar o spec-wave:spec'. Só vale para Features — Spike, RFC e Bug não usam spec."
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Bash(gh issue *)
|
|
6
6
|
- Bash(npx @spec-wave/cli@latest *)
|
|
@@ -9,7 +9,7 @@ allowed-tools:
|
|
|
9
9
|
|
|
10
10
|
# spec-wave spec — especificação funcional (1º documento)
|
|
11
11
|
|
|
12
|
-
> **Regra fundamental: nunca escreva o `spec.md` à mão.** Quem gera é o spec-wave — pelo Action ou pela CLI local. É isso que garante que o arquivo
|
|
12
|
+
> **Regra fundamental: nunca escreva o `spec.md` à mão.** Quem gera é o spec-wave — pelo Action ou pela CLI local. É isso que garante que o arquivo chegue à main por Pull Request e seja referenciado na issue. Exceção: revisar/melhorar um `spec.md` já gerado (aí sim use Edit).
|
|
13
13
|
|
|
14
14
|
## Dois modos, mesmo resultado
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ allowed-tools:
|
|
|
18
18
|
| **Action** | aplicar a label `spec-wave:spec` | fluxo assíncrono; roda no CI, você acompanha pela issue |
|
|
19
19
|
| **Local** | `npx @spec-wave/cli@latest generate-spec --issue-number <n>` | você quer o documento **agora**, nesta sessão, e iterar em cima dele |
|
|
20
20
|
|
|
21
|
-
O modo é detectado pelo ambiente — fora do Actions, a CLI roda em modo local automaticamente. Os dois geram o arquivo,
|
|
21
|
+
O modo é detectado pelo ambiente — fora do Actions, a CLI roda em modo local automaticamente. Os dois geram o arquivo, abrem um **Pull Request** com ele, comentam na issue e removem a label de gatilho. **Pergunte ao usuário qual ele quer** se não estiver claro; na dúvida numa sessão interativa, prefira o local (o resultado aparece em segundos, em vez de exigir acompanhar o Action).
|
|
22
22
|
|
|
23
23
|
> Local exige a credencial de IA no seu ambiente (`OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY` ou o login do Claude Code, se o provider for `claude-oauth`) e um `.spec-wave.json` no repositório. Para conduzir o fluxo inteiro sem gastar minutos de Actions, veja a skill **run**.
|
|
24
24
|
|
|
@@ -36,7 +36,7 @@ O modo é detectado pelo ambiente — fora do Actions, a CLI roda em modo local
|
|
|
36
36
|
```bash
|
|
37
37
|
npx @spec-wave/cli@latest generate-spec --issue-number <número>
|
|
38
38
|
```
|
|
39
|
-
O comando imprime `Modo de execução: local`, gera
|
|
39
|
+
O comando imprime `Modo de execução: local`, gera e abre o Pull Request. Nada é gravado no seu clone — o documento vive no PR até o merge.
|
|
40
40
|
|
|
41
41
|
**Action** — assíncrono:
|
|
42
42
|
```bash
|