@spec-wave/cli 0.27.0 → 0.29.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-graphql.mjs +37 -0
- package/src/api/github-rest.mjs +48 -0
- package/src/cli.mjs +51 -2
- package/src/commands/audit.mjs +280 -0
- package/src/commands/doctor.mjs +40 -16
- package/src/commands/implement.mjs +19 -2
- package/src/commands/install-skill.mjs +18 -8
- package/src/commands/merge.mjs +292 -0
- package/src/commands/move.mjs +26 -11
- package/src/commands/order.mjs +42 -0
- package/src/commands/preflight.mjs +322 -0
- package/src/commands/run.mjs +4 -3
- package/src/commands/update.mjs +143 -12
- package/src/lib/board.mjs +18 -2
- package/src/lib/critique.mjs +64 -8
- package/src/lib/pr-branch.mjs +96 -7
- package/src/lib/pr-step.mjs +12 -7
- package/src/lib/spec-audit.mjs +372 -0
- package/src/lib/tech-context.mjs +20 -14
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/README.md +5 -0
- package/src/plugin/skills/audit/SKILL.md +34 -0
- package/src/plugin/skills/audit/model-prompt.critique.md +34 -0
- package/src/plugin/skills/merge/SKILL.md +34 -0
- package/src/plugin/skills/order/SKILL.md +1 -0
- package/src/plugin/skills/plan/model-prompt.md +1 -0
- package/src/plugin/skills/plan/reference/tech-context.md +6 -0
- package/src/plugin/skills/preparar-feature/SKILL.md +247 -0
- package/src/plugin/skills/preparar-feature/reference/critica.md +88 -0
- package/src/plugin/skills/preparar-specs/SKILL.md +191 -0
- package/src/plugin/skills/preparar-specs/reference/armadilhas.md +209 -0
- package/src/plugin/skills/preparar-specs/reference/revisao.md +110 -0
- package/src/plugin/skills/update/SKILL.md +10 -4
- package/src/plugin/skills/workflow/SKILL.md +6 -1
- package/src/templates/config/tech_context.yml +13 -0
- package/src/templates/skill/SKILL.md +36 -7
- package/src/templates/workflows/qa.yml +9 -1
package/src/lib/critique.mjs
CHANGED
|
@@ -44,12 +44,16 @@ export const CRITIQUE_TOOL_NAME = 'registrar_findings';
|
|
|
44
44
|
// Rótulo do artefato auditado, por contexto — usado no cabeçalho do comentário.
|
|
45
45
|
const KIND_LABEL = {
|
|
46
46
|
plan: 'plan.md', stories: 'decomposition.md', bug: 'bug.md', spec: 'spec.md',
|
|
47
|
+
conjunto: 'specs da milestone (conjunto)',
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
// Prompt por tipo de auditoria. 'plan' audita o plan.md contra a spec;
|
|
50
|
-
// 'stories' audita a decomposição proposta contra spec + plan
|
|
51
|
+
// 'stories' audita a decomposição proposta contra spec + plan; 'conjunto'
|
|
52
|
+
// audita TODAS as specs de uma milestone entre si — é a única cujo objeto é a
|
|
53
|
+
// relação entre documentos, não um documento.
|
|
51
54
|
const KIND_PROMPT = {
|
|
52
55
|
plan: 'plan/critique', stories: 'decompose/critique', bug: 'bug/critique', spec: 'spec/critique',
|
|
56
|
+
conjunto: 'audit/critique',
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
// A decomposição virou arquivo revisável (decomposition.md): um finding só é
|
|
@@ -62,6 +66,13 @@ const ANCHOR_RULE = `Cada finding DEVE citar, no campo "anchor", o trecho audita
|
|
|
62
66
|
- "geral" quando o problema for da decomposição como um todo (ex.: requisito da spec que nenhuma Story cobre).
|
|
63
67
|
Use EXATAMENTE os números que aparecem nos títulos "## Story N — ..." e "### Task N.M — ..." do documento.`;
|
|
64
68
|
|
|
69
|
+
// No conjunto o achado vive ENTRE documentos: sem dizer quais, o leitor relê a
|
|
70
|
+
// milestone inteira procurando o par. O campo é validado como array de números
|
|
71
|
+
// de issue — é o equivalente da âncora "Story N" para este contexto.
|
|
72
|
+
const FEATURES_RULE = `Cada finding DEVE listar, no campo "features", os números das issues das Features envolvidas
|
|
73
|
+
(ex.: [412, 415] para uma contradição entre as duas; [412] quando o problema é de uma spec só,
|
|
74
|
+
visto à luz das outras). Use EXATAMENTE os números que aparecem nos títulos "## spec.md — #N ..." fornecidos.`;
|
|
75
|
+
|
|
65
76
|
/**
|
|
66
77
|
* Monta o system prompt da crítica: corpo editável + contrato de máquina.
|
|
67
78
|
*
|
|
@@ -79,7 +90,8 @@ Use EXATAMENTE os números que aparecem nos títulos "## Story N — ..." e "###
|
|
|
79
90
|
*/
|
|
80
91
|
function buildSystemPrompt(kind, cwd) {
|
|
81
92
|
const prompt = loadPrompt(KIND_PROMPT[kind] || KIND_PROMPT.plan, ...(cwd ? [{ cwd }] : []));
|
|
82
|
-
const anchor = kind === 'stories' ? `\n\n${ANCHOR_RULE}`
|
|
93
|
+
const anchor = kind === 'stories' ? `\n\n${ANCHOR_RULE}`
|
|
94
|
+
: kind === 'conjunto' ? `\n\n${FEATURES_RULE}` : '';
|
|
83
95
|
|
|
84
96
|
const contract = `## Contrato de saída
|
|
85
97
|
|
|
@@ -134,6 +146,17 @@ function critiqueJsonSchema(kind) {
|
|
|
134
146
|
};
|
|
135
147
|
required.push('anchor');
|
|
136
148
|
}
|
|
149
|
+
if (kind === 'conjunto') {
|
|
150
|
+
properties.features = {
|
|
151
|
+
type: 'array',
|
|
152
|
+
items: { type: 'integer' },
|
|
153
|
+
minItems: 1,
|
|
154
|
+
description:
|
|
155
|
+
'Números das issues das Features envolvidas no finding — os "#N" dos títulos ' +
|
|
156
|
+
'"## spec.md — #N ..." fornecidos.',
|
|
157
|
+
};
|
|
158
|
+
required.push('features');
|
|
159
|
+
}
|
|
137
160
|
return {
|
|
138
161
|
type: 'object',
|
|
139
162
|
properties: {
|
|
@@ -245,10 +268,23 @@ export function validateCritiquePayload(payload) {
|
|
|
245
268
|
);
|
|
246
269
|
}
|
|
247
270
|
const extra = itemKeys.filter(
|
|
248
|
-
k => k !== 'severity' && k !== 'text' && k !== 'anchor' && k !== 'quote');
|
|
271
|
+
k => k !== 'severity' && k !== 'text' && k !== 'anchor' && k !== 'quote' && k !== 'features');
|
|
249
272
|
if (extra.length > 0) {
|
|
250
273
|
throw new CritiqueSchemaError(`${at} tem campo(s) não reconhecido(s): ${extra.join(', ')}.`);
|
|
251
274
|
}
|
|
275
|
+
// `features` (kind 'conjunto'): quais issues o finding atravessa. Fora do
|
|
276
|
+
// formato é erro de schema — a localização é a metade do valor do achado.
|
|
277
|
+
let features = null;
|
|
278
|
+
if ('features' in item) {
|
|
279
|
+
if (!Array.isArray(item.features)
|
|
280
|
+
|| item.features.length === 0
|
|
281
|
+
|| !item.features.every(n => Number.isInteger(n) && n > 0)) {
|
|
282
|
+
throw new CritiqueSchemaError(
|
|
283
|
+
`${at}.features deveria ser um array não-vazio de números de issue, veio ${describe(item.features)}.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
features = [...new Set(item.features)];
|
|
287
|
+
}
|
|
252
288
|
// Normaliza só caixa e espaço — isso não é ambiguidade semântica. "GRAVE"
|
|
253
289
|
// passa; "gravíssimo", "critical" e "high" NÃO.
|
|
254
290
|
const severity = typeof item.severity === 'string' ? item.severity.trim().toLowerCase() : null;
|
|
@@ -268,6 +304,7 @@ export function validateCritiquePayload(payload) {
|
|
|
268
304
|
return {
|
|
269
305
|
severity,
|
|
270
306
|
...(anchor ? { anchor } : {}),
|
|
307
|
+
...(features ? { features } : {}),
|
|
271
308
|
...(quote ? { quote } : {}),
|
|
272
309
|
text: item.text.trim(),
|
|
273
310
|
};
|
|
@@ -721,6 +758,12 @@ const KIND_TRAILER = {
|
|
|
721
758
|
'até ser removida. Um bug com causa raiz errada produz correção errada — corrija o ' +
|
|
722
759
|
'`bug.md` e reaplique `spec-wave:bug`.'
|
|
723
760
|
: '_Findings menores não bloqueiam a triagem._'),
|
|
761
|
+
// O conjunto roda fora do ciclo de tentativas e não aplica label: o achado é
|
|
762
|
+
// decisão de PO entre duas specs, e o destino dele é a issue das duas pontas.
|
|
763
|
+
conjunto: (graves) => (graves
|
|
764
|
+
? '⛔ Há findings **graves** entre specs: comente nas issues DOS DOIS lados, apontando ' +
|
|
765
|
+
'uma para a outra, e resolva antes de gerar os planos — cada spec sozinha parece certa.'
|
|
766
|
+
: '_Nenhuma contradição entre as specs que exija decisão antes dos planos._'),
|
|
724
767
|
};
|
|
725
768
|
|
|
726
769
|
/**
|
|
@@ -782,8 +825,12 @@ export function renderCritiqueMarkdown({
|
|
|
782
825
|
return parts.join('\n\n');
|
|
783
826
|
}
|
|
784
827
|
|
|
828
|
+
// Localização do finding: a âncora ("Story N") ou, no conjunto, as Features
|
|
829
|
+
// que ele atravessa ("#412 × #415").
|
|
830
|
+
const onde = f => f.anchor
|
|
831
|
+
|| (f.features?.length ? f.features.map(n => `#${n}`).join(' × ') : '');
|
|
785
832
|
const bullets = list => list
|
|
786
|
-
.map(f => `- ${f
|
|
833
|
+
.map(f => `- ${onde(f) ? `**${onde(f)}** — ` : ''}${sanitizeFindingText(f.text)}`)
|
|
787
834
|
.join('\n');
|
|
788
835
|
if (graves.length > 0) parts.push(`### ❌ Graves\n\n${bullets(graves)}`);
|
|
789
836
|
if (menores.length > 0) parts.push(`### ⚠️ Menores\n\n${bullets(menores)}`);
|
|
@@ -791,7 +838,7 @@ export function renderCritiqueMarkdown({
|
|
|
791
838
|
parts.push(
|
|
792
839
|
'### ↘️ Rebaixados (vieram como graves, não bloqueiam)\n\n' +
|
|
793
840
|
rebaixados
|
|
794
|
-
.map(f => `- ${f
|
|
841
|
+
.map(f => `- ${onde(f) ? `**${onde(f)}** — ` : ''}${sanitizeFindingText(f.text)}\n` +
|
|
795
842
|
` - _Rebaixado: ${sanitizeFindingText(f.downgradeReason || 'não se sustenta como bloqueio')}._`)
|
|
796
843
|
.join('\n') +
|
|
797
844
|
'\n\n_Continuam valendo como observação. Se algum for mesmo grave, corrija o documento ' +
|
|
@@ -811,6 +858,7 @@ export function renderCritiqueMarkdown({
|
|
|
811
858
|
fingerprint: findingFingerprint({ kind, anchor: f.anchor, text: f.text }),
|
|
812
859
|
severity: f.severity,
|
|
813
860
|
anchor: f.anchor || null,
|
|
861
|
+
...(f.features?.length ? { features: f.features } : {}),
|
|
814
862
|
text: sanitizeFindingText(f.text),
|
|
815
863
|
...(f.downgraded ? { downgradedFrom: f.downgraded, downgradeReason: f.downgradeReason } : {}),
|
|
816
864
|
})),
|
|
@@ -832,8 +880,10 @@ export function renderCritiqueMarkdown({
|
|
|
832
880
|
* chamador decidir entre seguir com aviso e abortar.
|
|
833
881
|
*
|
|
834
882
|
* @param {object} params
|
|
835
|
-
* @param {'plan'|'stories'|'bug'} params.kind o que está sendo auditado
|
|
883
|
+
* @param {'plan'|'stories'|'bug'|'spec'|'conjunto'} params.kind o que está sendo auditado
|
|
836
884
|
* @param {string} [params.spec] conteúdo do spec.md
|
|
885
|
+
* @param {Array<{number:number, title:string, content:string}>} [params.specs]
|
|
886
|
+
* kind 'conjunto': TODAS as specs da milestone, uma seção por Feature
|
|
837
887
|
* @param {string} [params.plan] conteúdo do plan.md
|
|
838
888
|
* @param {string} [params.techContextYaml] tech_context serializado em YAML
|
|
839
889
|
* @param {string} [params.decomposition] conteúdo do decomposition.md
|
|
@@ -847,12 +897,17 @@ export function renderCritiqueMarkdown({
|
|
|
847
897
|
* @returns {Promise<{grave, findings, markdown, attempt, model}>}
|
|
848
898
|
*/
|
|
849
899
|
export async function runCritique({
|
|
850
|
-
kind, spec, plan, techContextYaml, decomposition, bugDoc, bugReport,
|
|
900
|
+
kind, spec, specs, plan, techContextYaml, decomposition, bugDoc, bugReport,
|
|
851
901
|
attempt = 1, maxAttempts = DEFAULT_MAX_CRITIQUE_ATTEMPTS,
|
|
852
902
|
model, labels = [], usage, cwd, decisions = null, standalone = false,
|
|
853
903
|
} = {}) {
|
|
854
904
|
const sections = [];
|
|
855
905
|
if (spec) sections.push(`## spec.md\n\n${spec}`);
|
|
906
|
+
// Conjunto: o título de cada seção carrega o "#N" que o campo `features` dos
|
|
907
|
+
// findings cita de volta — é o contrato de localização deste kind.
|
|
908
|
+
for (const s of specs || []) {
|
|
909
|
+
sections.push(`## spec.md — #${s.number} ${s.title}\n\n${s.content}`);
|
|
910
|
+
}
|
|
856
911
|
if (plan) sections.push(`## plan.md\n\n${plan}`);
|
|
857
912
|
if (techContextYaml) sections.push(`## tech_context\n\n\`\`\`yaml\n${techContextYaml}\n\`\`\``);
|
|
858
913
|
// Cerca de QUATRO crases: o decomposition.md contém cercas de três, e uma
|
|
@@ -898,7 +953,8 @@ export async function runCritique({
|
|
|
898
953
|
// sustenta não deve entrar no contador de tentativas nem bloquear o Action.
|
|
899
954
|
const findings = downgradeUnsupportedFindings(
|
|
900
955
|
report.value.findings,
|
|
901
|
-
[spec, plan, decomposition, bugDoc, techContextYaml]
|
|
956
|
+
[spec, ...(specs || []).map(s => s.content), plan, decomposition, bugDoc, techContextYaml]
|
|
957
|
+
.filter(Boolean),
|
|
902
958
|
);
|
|
903
959
|
const grave = findings.some(f => f.severity === 'grave');
|
|
904
960
|
const rebaixados = findings.filter(f => f.downgraded).length;
|
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
|
*
|
package/src/lib/pr-step.mjs
CHANGED
|
@@ -82,21 +82,26 @@ export function nextPrStep({
|
|
|
82
82
|
return { steps: [], warnings, reason: 'PR fechado sem merge.', blocked: null };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// Merge move até 🧪 QA mesmo sem aprovação formal: o autor não consegue
|
|
86
|
+
// aprovar o próprio PR (fluxo solo nunca teria QA por construção), e QA de
|
|
87
|
+
// verdade começa com o código integrado. O qa.yml tem o mesmo gatilho de
|
|
88
|
+
// merge — os dois modos continuam idênticos.
|
|
89
|
+
if (merged) warnings.push('PR já mergeado — merge move até 🧪 QA, com ou sem aprovação formal.');
|
|
90
|
+
if (changesRequestedAfterApproval && !merged) {
|
|
88
91
|
warnings.push('Há aprovação E pedido de mudanças: o Actions moveria assim mesmo — confirme com `--yes`.');
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
let steps = approved ? ['code-review', 'qa'] : ['code-review'];
|
|
94
|
+
let steps = (approved || merged) ? ['code-review', 'qa'] : ['code-review'];
|
|
92
95
|
if (only) steps = steps.filter(s => s === only);
|
|
93
96
|
|
|
94
97
|
return {
|
|
95
98
|
steps,
|
|
96
99
|
warnings,
|
|
97
|
-
reason:
|
|
98
|
-
? 'PR
|
|
99
|
-
:
|
|
100
|
+
reason: merged
|
|
101
|
+
? 'PR mergeado: board até 🧪 QA.'
|
|
102
|
+
: approved
|
|
103
|
+
? 'PR com review aprovada: board até 🧪 QA.'
|
|
104
|
+
: 'PR sem review aprovada: board até 👀 Code Review.',
|
|
100
105
|
blocked: null,
|
|
101
106
|
};
|
|
102
107
|
}
|