@spec-wave/cli 0.24.0 → 0.26.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/README.md +84 -0
- package/bin/spec-wave.mjs +4 -341
- package/package.json +1 -1
- package/src/api/github-rest.mjs +63 -1
- package/src/cli.mjs +400 -0
- package/src/commands/decompose.mjs +23 -16
- package/src/commands/doctor.mjs +32 -0
- package/src/commands/generate-bug.mjs +8 -14
- package/src/commands/generate-plan.mjs +2 -1
- package/src/commands/generate-spec.mjs +2 -1
- package/src/commands/mode.mjs +180 -0
- package/src/commands/run.mjs +491 -0
- package/src/commands/validate.mjs +22 -6
- package/src/config.mjs +4 -1
- package/src/lib/config-file.mjs +62 -0
- package/src/lib/doc-paths.mjs +51 -0
- package/src/lib/execution-mode.mjs +132 -0
- package/src/lib/next-step.mjs +412 -0
- package/src/lib/pr-step.mjs +102 -0
- package/src/lib/repo-links.mjs +84 -0
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/doctor/SKILL.md +1 -0
- package/src/plugin/skills/run/SKILL.md +76 -0
- package/src/plugin/skills/spec/SKILL.md +1 -1
- package/src/templates/skill/SKILL.md +24 -1
- package/src/templates/workflows/code-review.yml +5 -1
- package/src/templates/workflows/critique.yml +4 -0
- package/src/templates/workflows/decompose.yml +4 -0
- package/src/templates/workflows/generate-bug.yml +4 -0
- package/src/templates/workflows/generate-plan.yml +4 -0
- package/src/templates/workflows/generate-spec.yml +4 -0
- package/src/templates/workflows/qa.yml +6 -1
- package/src/templates/workflows/validate.yml +4 -0
|
@@ -5,12 +5,15 @@ import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-r
|
|
|
5
5
|
import { slugify } from '../lib/slugify.mjs';
|
|
6
6
|
import {
|
|
7
7
|
CONFIG_FILE, LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, LABEL_BUG, LABEL_BUG_APPROVED,
|
|
8
|
+
LABEL_READY, LABEL_PLAN_APPROVED,
|
|
8
9
|
REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS, REQUIRED_BUG_SECTIONS, labelNames,
|
|
9
10
|
} from '../config.mjs';
|
|
10
11
|
import { findIncompleteDocSigns } from '../lib/doc-completeness.mjs';
|
|
11
12
|
import { bugDocPaths, describeMissingSections } from '../lib/bug-doc.mjs';
|
|
12
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
13
14
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
15
|
+
import { executionMode } from '../lib/flow-run.mjs';
|
|
16
|
+
import { docBlobUrl } from '../lib/repo-links.mjs';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Validação do bug.md (RFC-004 §5).
|
|
@@ -55,7 +58,7 @@ async function validateBug({ token, owner, repo, issue, issueNumber, root }) {
|
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
await removeLabel(token, owner, repo, n,
|
|
61
|
+
await removeLabel(token, owner, repo, n, LABEL_READY);
|
|
59
62
|
|
|
60
63
|
if (errors.length > 0) {
|
|
61
64
|
await commentOnIssue(
|
|
@@ -65,18 +68,19 @@ async function validateBug({ token, owner, repo, issue, issueNumber, root }) {
|
|
|
65
68
|
`\n\nCorrija os problemas e adicione novamente a label \`spec-wave:ready\`.`
|
|
66
69
|
);
|
|
67
70
|
console.error('Validação falhou:', errors.join(', '));
|
|
68
|
-
|
|
71
|
+
return { ok: false, errors };
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
await addLabel(token, owner, repo, n, LABEL_BUG_APPROVED);
|
|
72
75
|
await commentOnIssue(
|
|
73
76
|
token, owner, repo, n,
|
|
74
77
|
'✅ **bug.md validado.**\n\n' +
|
|
75
|
-
`📄 [\`${fileRel}\`](
|
|
78
|
+
`📄 [\`${fileRel}\`](${docBlobUrl({ owner, repo, pathRel: fileRel, mode: executionMode(), root })})\n\n` +
|
|
76
79
|
'As seis seções obrigatórias estão presentes — reprodução, causa raiz e teste de ' +
|
|
77
80
|
'regressão inclusive. O bug pode ser aceito na triagem.'
|
|
78
81
|
);
|
|
79
82
|
console.log('bug.md validado.');
|
|
83
|
+
return { ok: true, errors: [] };
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/**
|
|
@@ -95,6 +99,17 @@ export function renderMissingSection(doc, { section, found }) {
|
|
|
95
99
|
: base;
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Valida os documentos de uma issue.
|
|
104
|
+
*
|
|
105
|
+
* Devolve o veredito em vez de chamar `process.exit(1)` na reprova: quem
|
|
106
|
+
* encerra o processo é o wrapper do `bin` (mesmo exit code no Actions), e assim
|
|
107
|
+
* o `run` consegue tratar a reprova como o desfecho ESPERADO que ela é — sem
|
|
108
|
+
* morrer no meio da cadeia, sem pular o `finally` e sem deixar o lock para trás.
|
|
109
|
+
*
|
|
110
|
+
* @param {{issueNumber: string|number}} params
|
|
111
|
+
* @returns {Promise<{ok: boolean, errors: string[]}>}
|
|
112
|
+
*/
|
|
98
113
|
export async function validate({ issueNumber }) {
|
|
99
114
|
const token = await resolveToken();
|
|
100
115
|
const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
|
|
@@ -174,7 +189,7 @@ export async function validate({ issueNumber }) {
|
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
// Remove trigger label
|
|
177
|
-
await removeLabel(token, owner, repo, parseInt(issueNumber, 10),
|
|
192
|
+
await removeLabel(token, owner, repo, parseInt(issueNumber, 10), LABEL_READY);
|
|
178
193
|
|
|
179
194
|
if (errors.length > 0) {
|
|
180
195
|
await commentOnIssue(
|
|
@@ -199,11 +214,11 @@ export async function validate({ issueNumber }) {
|
|
|
199
214
|
//
|
|
200
215
|
// Regenerar é decisão humana, e o comentário acima diz como e o que custa.
|
|
201
216
|
console.error('Validação falhou:', errors.join(', '));
|
|
202
|
-
|
|
217
|
+
return { ok: false, errors };
|
|
203
218
|
}
|
|
204
219
|
|
|
205
220
|
// Validação passou: adiciona label plan-approved.
|
|
206
|
-
await addLabel(token, owner, repo, parseInt(issueNumber, 10),
|
|
221
|
+
await addLabel(token, owner, repo, parseInt(issueNumber, 10), LABEL_PLAN_APPROVED);
|
|
207
222
|
|
|
208
223
|
await commentOnIssue(
|
|
209
224
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
@@ -216,4 +231,5 @@ export async function validate({ issueNumber }) {
|
|
|
216
231
|
);
|
|
217
232
|
|
|
218
233
|
console.log('Validação OK. Feature pronta para decomposição.');
|
|
234
|
+
return { ok: true, errors: [] };
|
|
219
235
|
}
|
package/src/config.mjs
CHANGED
|
@@ -407,6 +407,9 @@ export const LABEL_NEEDS_HUMAN = 'spec-wave:needs-human';
|
|
|
407
407
|
// pronta de uma que só foi movida à mão para ✅ Ready.
|
|
408
408
|
export const LABEL_PLAN_APPROVED = 'spec-wave:plan-approved';
|
|
409
409
|
// Gatilhos que REGERAM documento: presentes na issue, indicam etapa pendente.
|
|
410
|
+
// `spec-wave:ready` era o único gatilho sem constante — e por isso aparecia como
|
|
411
|
+
// string literal no validate, no YAML e em toda tabela que precisasse citá-lo.
|
|
412
|
+
export const LABEL_READY = 'spec-wave:ready';
|
|
410
413
|
export const LABEL_SPEC = 'spec-wave:spec';
|
|
411
414
|
export const LABEL_PLAN = 'spec-wave:plan';
|
|
412
415
|
// Achado GRAVE que o Tech Leader aceitou como risco conhecido. Fica na issue
|
|
@@ -422,7 +425,7 @@ export const TRIGGER_LABELS = [
|
|
|
422
425
|
{ name: LABEL_SPEC, color: 'BFD4F2', description: 'Gerar spec.md via GitHub Action' },
|
|
423
426
|
{ name: LABEL_PLAN, color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
|
|
424
427
|
{ name: LABEL_CRITIQUE, color: 'BFD4F2', description: 'Re-criticar o plan.md COMO ESTÁ, sem regerar' },
|
|
425
|
-
{ name:
|
|
428
|
+
{ name: LABEL_READY, color: '0E8A16', description: 'Validar spec+plan e mover para Ready' },
|
|
426
429
|
{ name: LABEL_PLAN_APPROVED, color: '0E8A16', description: 'Spec+plan validados com sucesso' },
|
|
427
430
|
{ name: LABEL_DECOMPOSE, color: 'BFD4F2', description: 'Gerar/re-criticar o rascunho da decomposição (decomposition.md)' },
|
|
428
431
|
{ name: LABEL_DECOMPOSE_APPLY, color: 'BFD4F2', description: 'Aplicar o decomposition.md revisado: criar Stories e Tasks' },
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Escrita do `.spec-wave.json` — um lugar só.
|
|
2
|
+
//
|
|
3
|
+
// Havia três escritores, cada um com a sua lógica: `init` monta o objeto do zero
|
|
4
|
+
// (e por isso descarta campos custom como `codeReview.linkMode` e `specKit`),
|
|
5
|
+
// `refresh` faz merge preservando o resto, e `update` tem a mesma lógica do
|
|
6
|
+
// refresh duplicada. Quem só precisa mexer em UM campo — o caso do `mode` — não
|
|
7
|
+
// tinha por onde.
|
|
8
|
+
//
|
|
9
|
+
// A serialização é parte do contrato, não detalhe: `update` compara o config
|
|
10
|
+
// byte a byte com o remoto para decidir se ele entra no PR (`lib/pr-branch.mjs`),
|
|
11
|
+
// então um `\n` a mais no fim faria o arquivo entrar em toda execução.
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
|
|
15
|
+
import { CONFIG_FILE } from '../config.mjs';
|
|
16
|
+
import { findConfigPath } from './project-root.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Serialização canônica do config (função PURA).
|
|
20
|
+
*
|
|
21
|
+
* @param {object} config
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function serializeConfig(config) {
|
|
25
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Aplica uma mutação ao `.spec-wave.json` preservando todo o resto.
|
|
30
|
+
*
|
|
31
|
+
* O mutator recebe uma CÓPIA e pode alterá-la no lugar ou devolver outro objeto.
|
|
32
|
+
* Gravação idempotente: conteúdo idêntico não reescreve o arquivo (não suja o
|
|
33
|
+
* mtime nem o `git status` de quem só consultou).
|
|
34
|
+
*
|
|
35
|
+
* @param {(config: object) => object|void} mutator
|
|
36
|
+
* @param {{cwd?: string}} [options]
|
|
37
|
+
* @returns {{configPath: string, config: object, changed: boolean}}
|
|
38
|
+
*/
|
|
39
|
+
export function updateConfig(mutator, { cwd = process.cwd() } = {}) {
|
|
40
|
+
const configPath = findConfigPath(cwd);
|
|
41
|
+
if (!configPath) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${CONFIG_FILE} não encontrado — rode \`npx @spec-wave/cli@latest init\` neste repositório.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
48
|
+
let current;
|
|
49
|
+
try {
|
|
50
|
+
current = JSON.parse(raw);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`${CONFIG_FILE} está corrompido (${err.message}) — corrija-o à mão antes.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const draft = structuredClone(current);
|
|
56
|
+
const next = mutator(draft) ?? draft;
|
|
57
|
+
const content = serializeConfig(next);
|
|
58
|
+
if (content === raw) return { configPath, config: next, changed: false };
|
|
59
|
+
|
|
60
|
+
writeFileSync(configPath, content);
|
|
61
|
+
return { configPath, config: next, changed: true };
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Onde mora cada documento do fluxo, a partir do TÍTULO da issue.
|
|
2
|
+
//
|
|
3
|
+
// O slug vem do título (`slugify`), então renomear a issue muda o diretório —
|
|
4
|
+
// motivo pelo qual o `run` compara o que as labels afirmam com o que existe no
|
|
5
|
+
// clone antes de decidir qualquer coisa.
|
|
6
|
+
//
|
|
7
|
+
// Funções PURAS: montam caminhos, não tocam o filesystem.
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { slugify } from './slugify.mjs';
|
|
12
|
+
|
|
13
|
+
export { bugDocPaths } from './bug-doc.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Diretório do documento por tipo de issue.
|
|
17
|
+
*
|
|
18
|
+
* Feature usa o mesmo `docs/features/<slug>` da spec/plan; RFC ganha o seu, já
|
|
19
|
+
* que não passa por spec/plan.
|
|
20
|
+
*
|
|
21
|
+
* @param {string|null} root raiz do repositório (ausente = process.cwd())
|
|
22
|
+
* @param {{title?: string}} issue
|
|
23
|
+
* @param {string|null} [type] tipo canônico ('Feature', 'RFC', ...)
|
|
24
|
+
* @returns {{slug: string, rel: string, dir: string}}
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDocDir(root, issue, type) {
|
|
27
|
+
const slug = slugify(issue?.title || '');
|
|
28
|
+
const rel = type === 'RFC' ? `docs/rfcs/${slug}` : `docs/features/${slug}`;
|
|
29
|
+
return { slug, rel, dir: path.resolve(root || process.cwd(), rel) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Caminhos dos três documentos de uma Feature/RFC (função PURA).
|
|
34
|
+
*
|
|
35
|
+
* @param {string|null} root
|
|
36
|
+
* @param {{title?: string}} issue
|
|
37
|
+
* @param {string|null} [type]
|
|
38
|
+
* @returns {{slug, dirRel, dirAbs, spec, plan, decomposition}} cada documento com {rel, abs}
|
|
39
|
+
*/
|
|
40
|
+
export function featureDocPaths(root, issue, type) {
|
|
41
|
+
const { slug, rel, dir } = resolveDocDir(root, issue, type);
|
|
42
|
+
const doc = name => ({ rel: `${rel}/${name}`, abs: path.join(dir, name) });
|
|
43
|
+
return {
|
|
44
|
+
slug,
|
|
45
|
+
dirRel: rel,
|
|
46
|
+
dirAbs: dir,
|
|
47
|
+
spec: doc('spec.md'),
|
|
48
|
+
plan: doc('plan.md'),
|
|
49
|
+
decomposition: doc('decomposition.md'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Onde o fluxo roda: no GitHub Actions ou nesta máquina.
|
|
2
|
+
//
|
|
3
|
+
// Duas fontes, de propósito, porque são dois leitores diferentes:
|
|
4
|
+
//
|
|
5
|
+
// • `.spec-wave.json` → `execution.mode` — é o que a CLI e a skill leem para
|
|
6
|
+
// saber se devem aplicar a label (Actions) ou chamar `spec-wave run` (local);
|
|
7
|
+
// • variável de repositório `SPEC_WAVE_EXECUTION` — é o que o GitHub avalia no
|
|
8
|
+
// `if:` de cada job. Sem ela, o workflow roda mesmo com o config dizendo
|
|
9
|
+
// "local", e o minuto é cobrado do mesmo jeito.
|
|
10
|
+
//
|
|
11
|
+
// Divergir é o estado perigoso, não um detalhe: o usuário acha que desligou e
|
|
12
|
+
// não desligou. Por isso a comparação vira função PURA aqui, e tanto o comando
|
|
13
|
+
// `mode` quanto o `doctor` dizem a mesma coisa.
|
|
14
|
+
//
|
|
15
|
+
// NÃO confundir com `executionMode()` de `lib/flow-run.mjs`: aquele responde
|
|
16
|
+
// "onde este processo está rodando AGORA" (GITHUB_ACTIONS); este responde "onde
|
|
17
|
+
// o repositório decidiu que o fluxo roda".
|
|
18
|
+
|
|
19
|
+
export const EXECUTION_MODES = ['actions', 'local'];
|
|
20
|
+
export const DEFAULT_EXECUTION_MODE = 'actions';
|
|
21
|
+
|
|
22
|
+
/** Nome da variável de repositório consultada pelo `if:` dos workflows. */
|
|
23
|
+
export const EXECUTION_VARIABLE = 'SPEC_WAVE_EXECUTION';
|
|
24
|
+
|
|
25
|
+
/** Expressão que os templates de workflow carregam — usada pelo doctor e pelos testes. */
|
|
26
|
+
export const EXECUTION_GUARD = `vars.${EXECUTION_VARIABLE} != 'local'`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Modo declarado no `.spec-wave.json` (função PURA).
|
|
30
|
+
*
|
|
31
|
+
* @param {object|null} config
|
|
32
|
+
* @returns {'actions'|'local'}
|
|
33
|
+
*/
|
|
34
|
+
export function configuredMode(config) {
|
|
35
|
+
const mode = String(config?.execution?.mode || '').toLowerCase();
|
|
36
|
+
return EXECUTION_MODES.includes(mode) ? mode : DEFAULT_EXECUTION_MODE;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Valor que a variável de repositório deve ter para um modo (função PURA).
|
|
41
|
+
*
|
|
42
|
+
* `null` significa "a variável não deve existir" — ausência é o estado default
|
|
43
|
+
* de qualquer repo, e é o que mantém os workflows armados sem depender de nada.
|
|
44
|
+
*
|
|
45
|
+
* @param {'actions'|'local'} mode
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
export function variableValueFor(mode) {
|
|
49
|
+
return mode === 'local' ? 'local' : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A variável do repositório precisa ser escrita? (função PURA)
|
|
54
|
+
*
|
|
55
|
+
* Três estados entram, não dois: `string` (valor lido), `null` (lida, e não
|
|
56
|
+
* existe) e `undefined` (NÃO deu para ler — 403, a variável exige admin).
|
|
57
|
+
*
|
|
58
|
+
* `undefined` conta como "precisa escrever". Tratá-lo como "já está certo"
|
|
59
|
+
* fazia o comando pular a escrita e ainda anunciar que config e variável
|
|
60
|
+
* coincidiam — o usuário saía achando que desligou o CI, com os workflows
|
|
61
|
+
* armados. Não conseguir ler quase sempre significa não conseguir escrever;
|
|
62
|
+
* tentar e falhar com a mensagem certa é honesto, não tentar e declarar
|
|
63
|
+
* sucesso não é.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} params
|
|
66
|
+
* @param {string|null|undefined} params.variable valor remoto
|
|
67
|
+
* @param {string|null} params.expected valor que o modo-alvo exige (null = ausente)
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function shouldWriteVariable({ variable, expected }) {
|
|
71
|
+
return variable !== expected;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Diagnóstico do modo de execução (função PURA).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} params
|
|
78
|
+
* @param {'actions'|'local'} params.configured modo no .spec-wave.json
|
|
79
|
+
* @param {string|null|undefined} params.variable valor remoto (undefined = não verificável)
|
|
80
|
+
* @param {string[]} [params.unguardedWorkflows] workflows instalados SEM a guarda
|
|
81
|
+
* @returns {{status: 'ok'|'warn'|'problem', summary: string, notes: string[], fixes: string[]}}
|
|
82
|
+
*/
|
|
83
|
+
export function describeModeState({ configured, variable, unguardedWorkflows = [] }) {
|
|
84
|
+
const esperado = variableValueFor(configured);
|
|
85
|
+
const notes = [];
|
|
86
|
+
const fixes = [];
|
|
87
|
+
let status = 'ok';
|
|
88
|
+
|
|
89
|
+
notes.push(`Modo declarado no .spec-wave.json: **${configured}**.`);
|
|
90
|
+
|
|
91
|
+
if (variable === undefined) {
|
|
92
|
+
status = 'warn';
|
|
93
|
+
notes.push(
|
|
94
|
+
`Variável \`${EXECUTION_VARIABLE}\` não verificável (requer admin no repo) — ` +
|
|
95
|
+
'não dá para afirmar daqui se os workflows estão armados.'
|
|
96
|
+
);
|
|
97
|
+
} else if (variable === esperado) {
|
|
98
|
+
notes.push(esperado === null
|
|
99
|
+
? `Variável \`${EXECUTION_VARIABLE}\` ausente, como deve ser no modo actions.`
|
|
100
|
+
: `Variável \`${EXECUTION_VARIABLE}\`=${variable} no repositório.`);
|
|
101
|
+
} else if (configured === 'local') {
|
|
102
|
+
// O caso que custa dinheiro: config diz local, GitHub continua disparando.
|
|
103
|
+
status = 'problem';
|
|
104
|
+
notes.push(
|
|
105
|
+
`Modo local, mas \`${EXECUTION_VARIABLE}\` ${variable === null ? 'não existe' : `vale "${variable}"`} ` +
|
|
106
|
+
'no repositório — os workflows CONTINUAM disparando e consumindo minutos.'
|
|
107
|
+
);
|
|
108
|
+
fixes.push('Rode `npx @spec-wave/cli@latest mode local` (ou defina a variável em Settings → Actions → Variables).');
|
|
109
|
+
} else {
|
|
110
|
+
status = 'problem';
|
|
111
|
+
notes.push(
|
|
112
|
+
`Modo actions, mas \`${EXECUTION_VARIABLE}\`="${variable}" está setada — os workflows ` +
|
|
113
|
+
'são pulados e nada roda até alguém rodar o comando local.'
|
|
114
|
+
);
|
|
115
|
+
fixes.push('Rode `npx @spec-wave/cli@latest mode actions` para remover a variável.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (configured === 'local' && unguardedWorkflows.length > 0) {
|
|
119
|
+
status = 'problem';
|
|
120
|
+
notes.push(
|
|
121
|
+
`Workflows instalados sem a guarda \`${EXECUTION_GUARD}\`: ${unguardedWorkflows.join(', ')} — ` +
|
|
122
|
+
'esses vão rodar de qualquer jeito.'
|
|
123
|
+
);
|
|
124
|
+
fixes.push('Rode `npx @spec-wave/cli@latest update` para publicar os workflows com a guarda.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const summary = configured === 'local'
|
|
128
|
+
? 'Execução local: os comandos rodam nesta máquina (`spec-wave run <issue>`).'
|
|
129
|
+
: 'Execução no GitHub Actions: as labels de gatilho disparam os workflows.';
|
|
130
|
+
|
|
131
|
+
return { status, summary, notes, fixes };
|
|
132
|
+
}
|