@spec-wave/cli 0.33.0 → 0.34.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 +28 -0
- package/src/api/github-rest.mjs +24 -0
- package/src/cli.mjs +6 -2
- package/src/commands/audit.mjs +10 -1
- package/src/commands/dev-agent.mjs +20 -4
- package/src/commands/doctor.mjs +107 -13
- package/src/commands/implement.mjs +167 -34
- package/src/commands/merge.mjs +18 -0
- package/src/commands/update.mjs +55 -10
- package/src/commands/validate.mjs +14 -0
- package/src/config.mjs +57 -4
- package/src/lib/bug-context.mjs +17 -6
- package/src/lib/commit-trailers.mjs +134 -0
- package/src/lib/delivery-mode.mjs +26 -0
- package/src/lib/rate-budget.mjs +36 -0
- package/src/lib/spec-audit.mjs +135 -19
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/audit/SKILL.md +2 -2
- package/src/plugin/skills/doctor/SKILL.md +6 -2
- package/src/plugin/skills/implement/SKILL.md +8 -2
- package/src/plugin/skills/merge/SKILL.md +1 -0
- package/src/plugin/skills/spec/model-prompt.md +1 -1
- package/src/setup/labels.mjs +6 -5
- package/src/templates/issue/spec-template.md +7 -2
- package/src/templates/skill/SKILL.md +14 -4
|
@@ -7,6 +7,7 @@ import { resolveToken } from '../api/auth.mjs';
|
|
|
7
7
|
import {
|
|
8
8
|
CONFIG_FILE, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_DONE, STAGE_ORDER,
|
|
9
9
|
PROGRESS_TODO, PROGRESS_IN_PROGRESS, PROGRESS_DONE, labelNames,
|
|
10
|
+
TRAILER_STORY, TRAILER_TASKS, TRAILER_AGENT,
|
|
10
11
|
} from '../config.mjs';
|
|
11
12
|
import { getIssue, listIssueComments, listBlockedBy, getFileContent } from '../api/github-rest.mjs';
|
|
12
13
|
import { listSubIssues, getIssueParent } from '../api/github-graphql.mjs';
|
|
@@ -23,6 +24,9 @@ import { extractPathsFromPlan, buildCodeDigest } from '../lib/code-digest.mjs';
|
|
|
23
24
|
import { findConfigPath, resolveFromRoot, loadConfig } from '../lib/project-root.mjs';
|
|
24
25
|
import { loadFeatureStories, loadBoardSnapshot, cachedSubIssues } from '../lib/story-graph.mjs';
|
|
25
26
|
import { resolveCacheTtl } from '../lib/net-cache.mjs';
|
|
27
|
+
import { deliveryMode } from '../lib/delivery-mode.mjs';
|
|
28
|
+
import { renderTrailers, missingTrailers, readLocalCommitMessages } from '../lib/commit-trailers.mjs';
|
|
29
|
+
import { currentGitBranch } from '../lib/repo-links.mjs';
|
|
26
30
|
|
|
27
31
|
// Diretório onde montamos o arquivo de contexto entregue ao spec-kit.
|
|
28
32
|
const WORK_DIR = '.spec-wave';
|
|
@@ -170,9 +174,9 @@ function pushDigestSpecPlan(lines, { codeDigest, spec, plan, specPath, planPath
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
// Monta o markdown de contexto que será entregue ao spec-kit implement.
|
|
173
|
-
function buildContext({
|
|
177
|
+
export function buildContext({
|
|
174
178
|
type, issue, tasks, feature, siblingStories = [], spec, plan, specPath, planPath,
|
|
175
|
-
comments = [], codeDigest = null, blockedByWarnings = [],
|
|
179
|
+
comments = [], codeDigest = null, blockedByWarnings = [], mode = 'pr-stack',
|
|
176
180
|
}) {
|
|
177
181
|
const lines = [];
|
|
178
182
|
lines.push(`# Contexto de implementação — ${type} #${issue.number}`);
|
|
@@ -216,13 +220,35 @@ function buildContext({
|
|
|
216
220
|
lines.push(' 4. Só então avance para a próxima Task.');
|
|
217
221
|
lines.push('');
|
|
218
222
|
lines.push(`3. **Ao concluir TODA a Story** (todas as Tasks na Etapa ${STAGE_DONE}):`);
|
|
219
|
-
lines.push(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
lines.push(
|
|
223
|
+
lines.push(
|
|
224
|
+
` 1. Faça o **commit** de todas as mudanças da implementação. O assunto é livre — ` +
|
|
225
|
+
'inclua estes `git trailer`(s) no **rodapé** (após uma linha em branco), literalmente ' +
|
|
226
|
+
'assim, sem alterar as chaves:'
|
|
227
|
+
);
|
|
228
|
+
lines.push(' ```');
|
|
229
|
+
lines.push(renderTrailers({
|
|
230
|
+
story: issue.number, tasks: tasks.map(t => t.number),
|
|
231
|
+
agent: mode === 'agent' ? 'spec-wave-agent' : undefined,
|
|
232
|
+
}).split('\n').map(l => ` ${l}`).join('\n'));
|
|
233
|
+
lines.push(' ```');
|
|
234
|
+
if (mode === 'agent') {
|
|
235
|
+
// Sob o dev-agent, o daemon mantém UM branch e UM PR por Feature
|
|
236
|
+
// (agent/issue-<n>) e é ele quem abre o PR ao final — nunca o modelo.
|
|
237
|
+
// Instruir "abra o PR da Story" aqui é o que produzia um branch lateral
|
|
238
|
+
// (agent/issue-<n>-story-<m>) cujo trabalho nunca entra nesse PR.
|
|
239
|
+
lines.push(
|
|
240
|
+
' 2. Faça **push no branch atual** — **NÃO crie um branch novo** e ' +
|
|
241
|
+
'**NÃO abra Pull Request**: o agente já mantém um único branch e um único ' +
|
|
242
|
+
'Pull Request para a Feature inteira, aberto por ele mesmo ao final.'
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// Draft de propósito: numa pilha de Stories (um PR baseado no anterior),
|
|
246
|
+
// cada rebase em cascata dispararia o CI inteiro em PRs que ninguém vai
|
|
247
|
+
// mergear naquele estado. Rascunho não mergeia (o GitHub bloqueia) e o
|
|
248
|
+
// required check continua valendo: quem revisa marca o PR como pronto, o
|
|
249
|
+
// `ready_for_review` dispara o CI, e só então o merge destrava.
|
|
250
|
+
lines.push(` 2. Abra o **Pull Request** da Story #${issue.number} **como rascunho** (\`gh pr create --draft\`) — o CI não roda em rascunho; quem revisa marca o PR como pronto e é aí que os checks disparam.`);
|
|
251
|
+
}
|
|
226
252
|
lines.push(
|
|
227
253
|
` 3. **Avance a Etapa da Story #${issue.number} para ${STAGE_CODE_REVIEW}** ` +
|
|
228
254
|
`(reinicie o Status para ${PROGRESS_TODO}). As Tasks já estão em ${STAGE_DONE}.`
|
|
@@ -374,7 +400,7 @@ export function planFeatureImplementation(stories, {
|
|
|
374
400
|
export function buildFeatureContext({
|
|
375
401
|
feature, stories, skipped = [],
|
|
376
402
|
spec, plan, specPath, planPath,
|
|
377
|
-
comments = [], codeDigest = null, blockedByWarnings = [],
|
|
403
|
+
comments = [], codeDigest = null, blockedByWarnings = [], mode = 'pr-stack',
|
|
378
404
|
}) {
|
|
379
405
|
const lines = [];
|
|
380
406
|
lines.push(`# Contexto de implementação — Feature #${feature.number}`);
|
|
@@ -402,7 +428,32 @@ export function buildFeatureContext({
|
|
|
402
428
|
lines.push(` 1. **Ao começar:** Status da Task → **${PROGRESS_IN_PROGRESS}** (a Etapa continua ${STAGE_DEVELOPMENT}).`);
|
|
403
429
|
lines.push(' 2. **Implemente** a Task por completo.');
|
|
404
430
|
lines.push(` 3. **Ao concluir:** **avance a Task para a Etapa ${STAGE_DONE}** com Status **${PROGRESS_DONE}**.`);
|
|
405
|
-
|
|
431
|
+
// O rodapé do commit é o rastro verificável: assunto livre, mas os
|
|
432
|
+
// trailers abaixo são o contrato (item 6 do rfc/plano-hardening-agentes-
|
|
433
|
+
// 2026-08.md — sobrevivem a squash/rebase, ao contrário de qualquer
|
|
434
|
+
// convenção de assunto).
|
|
435
|
+
const trailerHint = mode === 'agent'
|
|
436
|
+
? `${TRAILER_STORY}: #<número da Story>\n${TRAILER_TASKS}: #<números das Tasks, separados por vírgula>\n${TRAILER_AGENT}: spec-wave-agent`
|
|
437
|
+
: `${TRAILER_STORY}: #<número da Story>\n${TRAILER_TASKS}: #<números das Tasks, separados por vírgula>`;
|
|
438
|
+
if (mode === 'agent') {
|
|
439
|
+
// Mesma razão do buildContext: o daemon mantém UM branch e UM PR para a
|
|
440
|
+
// Feature inteira e abre o PR ele mesmo ao final — o modelo não cria
|
|
441
|
+
// branch nem abre PR por Story aqui.
|
|
442
|
+
lines.push(
|
|
443
|
+
`3. **Ao concluir TODAS as Tasks da Story:** faça o **commit** (assunto livre; rodapé com os trailers abaixo), ` +
|
|
444
|
+
`faça **push no branch atual** (**NÃO crie branch novo, NÃO abra Pull Request**) ` +
|
|
445
|
+
`e **avance a Etapa da Story para ${STAGE_CODE_REVIEW}** (Status ${PROGRESS_TODO}).`
|
|
446
|
+
);
|
|
447
|
+
} else {
|
|
448
|
+
lines.push(
|
|
449
|
+
`3. **Ao concluir TODAS as Tasks da Story:** faça o **commit** (assunto livre; rodapé com os trailers abaixo), ` +
|
|
450
|
+
`abra o **Pull Request** da Story **como rascunho** (\`gh pr create --draft\` — o CI não roda em rascunho; ` +
|
|
451
|
+
`quem revisa marca o PR como pronto e os checks disparam) e **avance a Etapa da Story para ${STAGE_CODE_REVIEW}** (Status ${PROGRESS_TODO}).`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
lines.push(' ```');
|
|
455
|
+
lines.push(trailerHint.split('\n').map(l => ` ${l}`).join('\n'));
|
|
456
|
+
lines.push(' ```');
|
|
406
457
|
lines.push('4. Só então inicie a próxima Story.');
|
|
407
458
|
lines.push('');
|
|
408
459
|
lines.push(
|
|
@@ -411,17 +462,24 @@ export function buildFeatureContext({
|
|
|
411
462
|
`Enquanto houver Story pendente, a Feature permanece em ${STAGE_DEVELOPMENT}.`
|
|
412
463
|
);
|
|
413
464
|
lines.push('');
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
465
|
+
if (mode === 'agent') {
|
|
466
|
+
lines.push(
|
|
467
|
+
'**Ao terminar, informe no relatório final:** o agente mantém um único branch e um único ' +
|
|
468
|
+
'Pull Request para a Feature inteira, aberto por ele mesmo ao final — não há pilha de PRs a mergear aqui.'
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
// O trecho pós-implementação era o único do fluxo inteiramente manual — e o
|
|
472
|
+
// merge de PRs empilhados é ordem-dependente (apagar a branch do primeiro já
|
|
473
|
+
// fechou o segundo). O contexto termina apontando o comando que encapsula a
|
|
474
|
+
// sequência segura, em vez de deixar a mecânica por conta de quem mergeia.
|
|
475
|
+
lines.push(
|
|
476
|
+
'**Ao terminar, informe no relatório final:** os PRs ficam **empilhados** (cada um baseado no anterior) — ' +
|
|
477
|
+
'o merge é **ordem-dependente**. Depois da revisão humana (marcar cada PR como pronto), o merge é ' +
|
|
478
|
+
`\`npx @spec-wave/cli@latest merge ${feature.number}\`: ele mergeia na ordem das dependências, ` +
|
|
479
|
+
'reaponta as bases, move o board até 🧪 QA e só apaga as branches no fim. ' +
|
|
480
|
+
'**NUNCA** mergeie um PR da pilha com `--delete-branch` à mão — apagar a branch antes de reapontar o dependente fecha o PR seguinte.'
|
|
481
|
+
);
|
|
482
|
+
}
|
|
425
483
|
lines.push('');
|
|
426
484
|
lines.push(boardRuleBlockquote());
|
|
427
485
|
|
|
@@ -550,9 +608,14 @@ async function implementBug({ token, owner, repo, config, bug, dryRun, repoRoot
|
|
|
550
608
|
// dependências não legíveis — segue
|
|
551
609
|
}
|
|
552
610
|
|
|
611
|
+
const currentBranch = currentGitBranch(repoRoot);
|
|
612
|
+
const mode = deliveryMode({ branch: currentBranch });
|
|
613
|
+
if (mode === 'agent') {
|
|
614
|
+
p.log.info(`Modo dev-agent detectado (branch ${chalk.cyan(currentBranch)}) — o PR é aberto pelo agente ao final.`);
|
|
615
|
+
}
|
|
553
616
|
const context = buildBugContext({
|
|
554
617
|
bug: { number: issueNumber, title: bug.title, body: bug.body || '' },
|
|
555
|
-
bugDoc, parent, comments, codeDigest, blockedByWarnings, severity,
|
|
618
|
+
bugDoc, parent, comments, codeDigest, blockedByWarnings, severity, mode,
|
|
556
619
|
});
|
|
557
620
|
|
|
558
621
|
await writeContextAndRunSpecKit({
|
|
@@ -563,7 +626,12 @@ async function implementBug({ token, owner, repo, config, bug, dryRun, repoRoot
|
|
|
563
626
|
specPlan: { spec: null, plan: null, specPath: null, planPath: null },
|
|
564
627
|
context,
|
|
565
628
|
dryRun,
|
|
566
|
-
|
|
629
|
+
mode,
|
|
630
|
+
repoRoot,
|
|
631
|
+
expectedBranch: currentBranch,
|
|
632
|
+
outroSuccess: mode === 'agent'
|
|
633
|
+
? `Bug #${issueNumber} corrigido — o PR é aberto pelo próprio agente ao final.`
|
|
634
|
+
: `Bug #${issueNumber} corrigido — abra o PR com \`Fixes #${issueNumber}\`.`,
|
|
567
635
|
onSuccess: async () => {
|
|
568
636
|
await applyBoardMoves({
|
|
569
637
|
token,
|
|
@@ -577,6 +645,11 @@ async function implementBug({ token, owner, repo, config, bug, dryRun, repoRoot
|
|
|
577
645
|
|
|
578
646
|
async function implementFeature({ token, owner, repo, config, feature, featureDirOpt, dryRun, repoRoot, refresh = false }) {
|
|
579
647
|
const ttlSec = resolveCacheTtl(config);
|
|
648
|
+
const currentBranch = currentGitBranch(repoRoot);
|
|
649
|
+
const mode = deliveryMode({ branch: currentBranch });
|
|
650
|
+
if (mode === 'agent') {
|
|
651
|
+
p.log.info(`Modo dev-agent detectado (branch ${chalk.cyan(currentBranch)}) — sem branch/PR por Story.`);
|
|
652
|
+
}
|
|
580
653
|
|
|
581
654
|
// F1-F2. Stories da Feature com as arestas já resolvidas (lib/story-graph):
|
|
582
655
|
// dependency-map.json/decomposition.md ∪ linhas "Depende de:" do body — zero
|
|
@@ -748,6 +821,7 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
748
821
|
comments,
|
|
749
822
|
codeDigest,
|
|
750
823
|
blockedByWarnings,
|
|
824
|
+
mode,
|
|
751
825
|
});
|
|
752
826
|
await writeContextAndRunSpecKit({
|
|
753
827
|
config,
|
|
@@ -757,10 +831,16 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
757
831
|
specPlan,
|
|
758
832
|
context,
|
|
759
833
|
dryRun,
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
834
|
+
mode,
|
|
835
|
+
repoRoot,
|
|
836
|
+
expectedBranch: currentBranch,
|
|
837
|
+
outroSuccess: mode === 'agent'
|
|
838
|
+
? `${chalk.green('✓')} Implementação acionada para a Feature #${feature.number} ` +
|
|
839
|
+
`(${pending.length} story(ies) pendente(s)).\n` +
|
|
840
|
+
` Próximo: acompanhe o PR único da Feature; ele é aberto pelo próprio agente ao final.`
|
|
841
|
+
: `${chalk.green('✓')} Implementação acionada para a Feature #${feature.number} ` +
|
|
842
|
+
`(${pending.length} story(ies) pendente(s)).\n` +
|
|
843
|
+
` Próximo: acompanhe os PRs de cada Story; a Feature avança para ${STAGE_CODE_REVIEW} após a última.`,
|
|
764
844
|
onSuccess: async () => {
|
|
765
845
|
// Execução única para todas as pendentes: no sucesso, Tasks → Done e
|
|
766
846
|
// Stories → Code Review.
|
|
@@ -787,7 +867,9 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
787
867
|
});
|
|
788
868
|
}
|
|
789
869
|
|
|
790
|
-
export async function implement({
|
|
870
|
+
export async function implement({
|
|
871
|
+
issue: issueArg, featureDir: featureDirOpt, dryRun, refresh = false, verifyCommits = false,
|
|
872
|
+
}) {
|
|
791
873
|
const issueNumber = parseInt(String(issueArg).replace('#', ''), 10);
|
|
792
874
|
if (!Number.isInteger(issueNumber)) {
|
|
793
875
|
p.log.error(`Issue inválida: "${issueArg}". Use o número da issue, ex.: 12 ou #12.`);
|
|
@@ -979,15 +1061,28 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
979
1061
|
} catch { /* aviso é best-effort — segue sem ele */ }
|
|
980
1062
|
|
|
981
1063
|
// 5-6. Monta o contexto e aciona o spec-kit (comando configurável).
|
|
1064
|
+
const currentBranch = currentGitBranch(repoRoot);
|
|
1065
|
+
const mode = deliveryMode({ branch: currentBranch });
|
|
1066
|
+
if (mode === 'agent') {
|
|
1067
|
+
p.log.info(`Modo dev-agent detectado (branch ${chalk.cyan(currentBranch)}) — sem branch/PR novo para esta ${type}.`);
|
|
1068
|
+
}
|
|
982
1069
|
const context = buildContext({
|
|
983
1070
|
type, issue, tasks, feature, siblingStories, ...specPlan,
|
|
984
|
-
comments, codeDigest, blockedByWarnings,
|
|
1071
|
+
comments, codeDigest, blockedByWarnings, mode,
|
|
985
1072
|
});
|
|
986
1073
|
await writeContextAndRunSpecKit({
|
|
987
1074
|
config, issueNumber, type, title: issue.title, specPlan, context, dryRun,
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1075
|
+
mode, repoRoot, expectedBranch: currentBranch,
|
|
1076
|
+
verifyCommits,
|
|
1077
|
+
expectedTrailers: {
|
|
1078
|
+
stories: type === 'Story' ? [issue.number] : [],
|
|
1079
|
+
tasks: tasks.map(t => t.number),
|
|
1080
|
+
},
|
|
1081
|
+
outroSuccess: mode === 'agent'
|
|
1082
|
+
? `${chalk.green('✓')} Implementação acionada para ${type} #${issueNumber}.\n` +
|
|
1083
|
+
' Próximo: o board já foi atualizado; o PR é aberto pelo próprio agente ao final.'
|
|
1084
|
+
: `${chalk.green('✓')} Implementação acionada para ${type} #${issueNumber}.\n` +
|
|
1085
|
+
' Próximo: revise as mudanças e abra o PR — o board já foi atualizado.',
|
|
991
1086
|
onSuccess: async () => {
|
|
992
1087
|
await applyBoardMoves({ token, moves: planBoardMoves('success', {
|
|
993
1088
|
story: type === 'Story' ? { nodeId: issue.node_id, number: issue.number } : null,
|
|
@@ -1020,7 +1115,11 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
1020
1115
|
|
|
1021
1116
|
// Grava o arquivo de contexto e aciona o spec-kit (comando configurável) —
|
|
1022
1117
|
// fecho comum dos modos Feature e Story/Task.
|
|
1023
|
-
async function writeContextAndRunSpecKit({
|
|
1118
|
+
async function writeContextAndRunSpecKit({
|
|
1119
|
+
config, issueNumber, type, title, specPlan, context, dryRun, outroSuccess, onSuccess,
|
|
1120
|
+
mode = 'pr-stack', repoRoot = null, expectedBranch = null,
|
|
1121
|
+
verifyCommits = false, expectedTrailers = null,
|
|
1122
|
+
}) {
|
|
1024
1123
|
mkdirSync(WORK_DIR, { recursive: true });
|
|
1025
1124
|
const tasksFile = path.join(WORK_DIR, `implement-${issueNumber}.md`);
|
|
1026
1125
|
writeFileSync(tasksFile, context);
|
|
@@ -1075,6 +1174,40 @@ async function writeContextAndRunSpecKit({ config, issueNumber, type, title, spe
|
|
|
1075
1174
|
return;
|
|
1076
1175
|
}
|
|
1077
1176
|
|
|
1177
|
+
// Guarda barata contra o sintoma do item 1 do rfc/plano-hardening-agentes-
|
|
1178
|
+
// 2026-08.md: sob o dev-agent, se o executor saiu do branch único do agente
|
|
1179
|
+
// (por exemplo criando um branch lateral por Story), o commit foi parar
|
|
1180
|
+
// fora do PR que o daemon abre ao final. O aviso aparece na própria saída
|
|
1181
|
+
// em vez de custar um ciclo de "onde foi parar meu trabalho".
|
|
1182
|
+
if (mode === 'agent' && expectedBranch) {
|
|
1183
|
+
const branchAfter = currentGitBranch(repoRoot);
|
|
1184
|
+
if (branchAfter && branchAfter !== expectedBranch) {
|
|
1185
|
+
p.log.warn(
|
|
1186
|
+
`⚠️ o executor saiu de ${chalk.cyan(expectedBranch)} para ${chalk.cyan(branchAfter)} — ` +
|
|
1187
|
+
'se algo foi commitado nesse branch, mova o trabalho de volta antes do daemon abrir o PR.'
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// --verify-commits (item 6 do rfc/plano-hardening-agentes-2026-08.md): lê
|
|
1193
|
+
// os commits recentes do checkout e confere se o rastro (trailer) do que
|
|
1194
|
+
// acabou de ser implementado está lá — best-effort, nunca bloqueia.
|
|
1195
|
+
if (verifyCommits && expectedTrailers) {
|
|
1196
|
+
const messages = readLocalCommitMessages(repoRoot || process.cwd());
|
|
1197
|
+
const { missingStories, missingTasks } = missingTrailers(messages, expectedTrailers);
|
|
1198
|
+
if (missingStories.length === 0 && missingTasks.length === 0) {
|
|
1199
|
+
p.log.success('Trailers de rastro (Spec-Wave-Story/Tasks) confirmados nos commits recentes.');
|
|
1200
|
+
} else {
|
|
1201
|
+
const partes = [];
|
|
1202
|
+
if (missingStories.length) partes.push(`Story ${missingStories.map(n => `#${n}`).join(', ')}`);
|
|
1203
|
+
if (missingTasks.length) partes.push(`Task(s) ${missingTasks.map(n => `#${n}`).join(', ')}`);
|
|
1204
|
+
p.log.warn(
|
|
1205
|
+
`⚠️ Sem trailer de rastro para: ${partes.join('; ')} — nenhum commit recente traz ` +
|
|
1206
|
+
'`Spec-Wave-Story`/`Spec-Wave-Tasks`. Confira se o commit saiu como instruído no contexto.'
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1078
1211
|
if (onSuccess) await onSuccess();
|
|
1079
1212
|
p.outro(outroSuccess);
|
|
1080
1213
|
}
|
package/src/commands/merge.mjs
CHANGED
|
@@ -22,8 +22,10 @@ import chalk from 'chalk';
|
|
|
22
22
|
import { resolveToken } from '../api/auth.mjs';
|
|
23
23
|
import {
|
|
24
24
|
getIssue, getPR, getRepoDefaultBranch, updatePRBase, mergePR, deleteBranch,
|
|
25
|
+
listPullRequestCommitMessages,
|
|
25
26
|
} from '../api/github-rest.mjs';
|
|
26
27
|
import { listIssuePullRequests } from '../api/github-graphql.mjs';
|
|
28
|
+
import { missingTrailers } from '../lib/commit-trailers.mjs';
|
|
27
29
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
28
30
|
import { orderStories } from '../lib/dependencies.mjs';
|
|
29
31
|
import { resolveRepoContext, loadConfig } from '../lib/project-root.mjs';
|
|
@@ -242,6 +244,22 @@ export async function merge({
|
|
|
242
244
|
for (const a of plano.avisos) p.log.warn(a);
|
|
243
245
|
for (const b of plano.bloqueios) p.log.error(b);
|
|
244
246
|
|
|
247
|
+
// Conferência de trailers (item 6 do rfc/plano-hardening-agentes-2026-08.md):
|
|
248
|
+
// best-effort, nunca bloqueia o merge — nasce como relatório, não como
|
|
249
|
+
// guarda. Só as PRs que de fato vão ser mergeadas (plano.fila) valem a
|
|
250
|
+
// chamada extra por PR; falha de rede não afirma nada, só é pulada.
|
|
251
|
+
for (const { story, pr } of plano.fila) {
|
|
252
|
+
const messages = await listPullRequestCommitMessages(token, owner, repo, pr.number).catch(() => null);
|
|
253
|
+
if (messages === null) continue;
|
|
254
|
+
const { missingStories } = missingTrailers(messages, { stories: [story] });
|
|
255
|
+
if (missingStories.length > 0) {
|
|
256
|
+
p.log.warn(
|
|
257
|
+
`PR #${pr.number} (Story #${story}): nenhum commit traz o trailer \`Spec-Wave-Story: #${story}\` ` +
|
|
258
|
+
'— confira se é mesmo o trabalho certo antes de mergear.'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
245
263
|
if (plano.bloqueios.length > 0) {
|
|
246
264
|
p.outro('Bloqueado — merge parcial de pilha é o pior estado; resolva e rode de novo.');
|
|
247
265
|
process.exitCode = 1;
|
package/src/commands/update.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { getProjectSnapshot } from '../api/github-graphql.mjs';
|
|
|
9
9
|
import {
|
|
10
10
|
getFileContent, upsertFile, listLabels, createLabel, updateLabel, deleteLabel,
|
|
11
11
|
getRepoDefaultBranch, compareBranches, commitFilesToBranch, ensurePullRequest,
|
|
12
|
+
countOpenIssuesWithLabel,
|
|
12
13
|
} from '../api/github-rest.mjs';
|
|
13
14
|
import {
|
|
14
15
|
resolveBranchName, composePrTitle, composePrBody, buildCommitMessage,
|
|
@@ -116,18 +117,21 @@ export function skillRepoTargets(job, repoRoot) {
|
|
|
116
117
|
* 0.13.0 mas ainda presente em todo repo inicializado com a 0.12.0. Só o
|
|
117
118
|
* namespace `spec-wave:` é considerado: labels do time não são da nossa conta.
|
|
118
119
|
*
|
|
119
|
-
* As labels de modelo (`spec-wave:model:<apelido>`) entram pelo `fileAi
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* As labels de modelo (`spec-wave:model:<apelido>`) entram pelo `fileAi`, e as
|
|
121
|
+
* de trilha do dev-agent (`spec-wave:dev-agent-b`, …) pelo `devAgent`: as duas
|
|
122
|
+
* são derivadas do `.spec-wave.json`, e tratá-las como desconhecidas fazia o
|
|
123
|
+
* update APAGAR o override de modelo por issue ou a fila de uma trilha
|
|
124
|
+
* paralela — mecanismos que destravam trabalho, não lixo a limpar. Passar os
|
|
125
|
+
* dois blocos também é o que faz o update CRIÁ-LAS, em vez de exigir
|
|
126
|
+
* `gh label create` à mão.
|
|
124
127
|
*
|
|
125
128
|
* @param {Array<{name,color,description}>} existing labels do repo
|
|
126
129
|
* @param {object} [fileAi] bloco `ai` do .spec-wave.json
|
|
130
|
+
* @param {object} [devAgent] bloco `devAgent` do .spec-wave.json
|
|
127
131
|
* @returns {{ missing: object[], changed: object[], orphan: object[] }}
|
|
128
132
|
*/
|
|
129
|
-
export function diffLabels(existing, fileAi) {
|
|
130
|
-
const wanted = allLabelsFor(fileAi);
|
|
133
|
+
export function diffLabels(existing, fileAi, devAgent) {
|
|
134
|
+
const wanted = allLabelsFor(fileAi, devAgent);
|
|
131
135
|
const byName = new Map((existing || []).map(l => [l.name, l]));
|
|
132
136
|
const missing = [];
|
|
133
137
|
const changed = [];
|
|
@@ -147,6 +151,30 @@ export function diffLabels(existing, fileAi) {
|
|
|
147
151
|
return { missing, changed, orphan };
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Decide se uma label órfã pode ser deletada (função PURA — testável sem rede).
|
|
156
|
+
*
|
|
157
|
+
* Deletar uma label no GitHub a remove de TODA issue aberta que a tem — é
|
|
158
|
+
* irreversível, e a label nem fica disponível para reaplicar. Por isso o
|
|
159
|
+
* default é conservador nos dois sentidos: `inUse` desconhecido (rede falhou
|
|
160
|
+
* ao contar) trava a remoção do mesmo jeito que `inUse > 0`, porque "não sei"
|
|
161
|
+
* não é "zero". `forceLabels` pula a checagem por completo — para quem sabe
|
|
162
|
+
* que a label é lixo de verdade.
|
|
163
|
+
*
|
|
164
|
+
* @param {{ inUse: number|null, forceLabels?: boolean }} params
|
|
165
|
+
* @returns {{ ok: boolean, motivo: string|null }}
|
|
166
|
+
*/
|
|
167
|
+
export function shouldDeleteOrphanLabel({ inUse, forceLabels = false }) {
|
|
168
|
+
if (forceLabels) return { ok: true, motivo: null };
|
|
169
|
+
if (inUse === null) {
|
|
170
|
+
return { ok: false, motivo: 'não foi possível confirmar o uso — mantida por precaução' };
|
|
171
|
+
}
|
|
172
|
+
if (inUse > 0) {
|
|
173
|
+
return { ok: false, motivo: `está em ${inUse} issue(s) aberta(s) — NÃO removida` };
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, motivo: null };
|
|
176
|
+
}
|
|
177
|
+
|
|
150
178
|
/**
|
|
151
179
|
* Regenera o conteúdo do .spec-wave.json a partir do Project remoto SEM GRAVAR.
|
|
152
180
|
*
|
|
@@ -186,11 +214,20 @@ async function buildConfigContent(token, config) {
|
|
|
186
214
|
* Label NUNCA entra num PR: é metadado do repositório, não arquivo versionado —
|
|
187
215
|
* não existe forma de propor a mudança para revisão.
|
|
188
216
|
*
|
|
217
|
+
* Órfã em uso não é removida por default (item 7 do
|
|
218
|
+
* rfc/plano-hardening-agentes-2026-08.md): apagar uma label no GitHub a
|
|
219
|
+
* remove de TODA issue aberta que a tem, e isso é irreversível — a label nem
|
|
220
|
+
* fica disponível para reaplicar. A guarda vale mesmo com `devAgent.queues`
|
|
221
|
+
* configurado certo: um erro de digitação no config não pode custar uma fila
|
|
222
|
+
* inteira. `forceLabels` pula a checagem, para quem sabe que a label é lixo
|
|
223
|
+
* de verdade e quer removê-la mesmo com issues abertas.
|
|
224
|
+
*
|
|
225
|
+
* @param {boolean} [forceLabels]
|
|
189
226
|
* @returns {Promise<{created: string[], updated: string[], removed: string[]}>}
|
|
190
227
|
* só o que REALMENTE passou — é o que o corpo do PR vai afirmar a quem
|
|
191
228
|
* revisa, e ele não pode prometer o que falhou.
|
|
192
229
|
*/
|
|
193
|
-
async function applyLabels(token, owner, repo, labelDiff) {
|
|
230
|
+
async function applyLabels(token, owner, repo, labelDiff, forceLabels = false) {
|
|
194
231
|
const done = { created: [], updated: [], removed: [] };
|
|
195
232
|
for (const label of labelDiff.missing) {
|
|
196
233
|
try {
|
|
@@ -212,6 +249,14 @@ async function applyLabels(token, owner, repo, labelDiff) {
|
|
|
212
249
|
}
|
|
213
250
|
for (const label of labelDiff.orphan) {
|
|
214
251
|
try {
|
|
252
|
+
const inUse = forceLabels
|
|
253
|
+
? 0
|
|
254
|
+
: await countOpenIssuesWithLabel(token, owner, repo, label.name).catch(() => null);
|
|
255
|
+
const decision = shouldDeleteOrphanLabel({ inUse, forceLabels });
|
|
256
|
+
if (!decision.ok) {
|
|
257
|
+
p.log.warn(`Label ${label.name}: ${decision.motivo}. Use --force-labels para remover mesmo assim.`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
215
260
|
await deleteLabel(token, owner, repo, label.name);
|
|
216
261
|
done.removed.push(label.name);
|
|
217
262
|
p.log.success(`Label descontinuada removida: ${label.name}`);
|
|
@@ -310,7 +355,7 @@ export async function update(options = {}) {
|
|
|
310
355
|
if (remote === null) repoFiles.push({ ...f, reason: 'ausente', local });
|
|
311
356
|
else if (remote !== local) repoFiles.push({ ...f, reason: 'desatualizado', local });
|
|
312
357
|
}
|
|
313
|
-
labelDiff = diffLabels(await listLabels(tk, owner, repo), config?.ai);
|
|
358
|
+
labelDiff = diffLabels(await listLabels(tk, owner, repo), config?.ai, config?.devAgent);
|
|
314
359
|
repoChecked = true;
|
|
315
360
|
s.stop('Repositório comparado.');
|
|
316
361
|
} catch (err) {
|
|
@@ -626,7 +671,7 @@ export async function update(options = {}) {
|
|
|
626
671
|
// Labels — sempre direto na base (metadado do repo, não versionável).
|
|
627
672
|
let labelResult = { created: [], updated: [], removed: [] };
|
|
628
673
|
if (labelTotal) {
|
|
629
|
-
labelResult = await applyLabels(await getToken(), owner, repo, labelDiff);
|
|
674
|
+
labelResult = await applyLabels(await getToken(), owner, repo, labelDiff, !!options.forceLabels);
|
|
630
675
|
}
|
|
631
676
|
|
|
632
677
|
// Arquivos do repo
|
|
@@ -16,6 +16,7 @@ import { loadArtifact } from '../lib/doc-source.mjs';
|
|
|
16
16
|
import { awaitingMergeBlock } from '../lib/artifact-pr.mjs';
|
|
17
17
|
import { isAwaitingMerge } from '../lib/doc-source.mjs';
|
|
18
18
|
import { getRepoDefaultBranch } from '../api/github-rest.mjs';
|
|
19
|
+
import { dependencySection } from '../lib/spec-audit.mjs';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Aborto por documento ainda em Pull Request.
|
|
@@ -241,6 +242,7 @@ export async function validate({ issueNumber }) {
|
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
// Check spec.md
|
|
245
|
+
const warnings = [];
|
|
244
246
|
if (spec.content == null) {
|
|
245
247
|
errors.push('❌ `spec.md` não encontrado em `' + specRel + '`');
|
|
246
248
|
} else {
|
|
@@ -253,6 +255,17 @@ export async function validate({ issueNumber }) {
|
|
|
253
255
|
for (const problem of findIncompleteDocSigns(specContent)) {
|
|
254
256
|
errors.push(`❌ \`spec.md\` parece incompleto: ${problem}`);
|
|
255
257
|
}
|
|
258
|
+
// Aviso, NUNCA erro (item 3e do rfc/plano-hardening-agentes-2026-08.md):
|
|
259
|
+
// "Dependências" não está em REQUIRED_SPEC_SECTIONS de propósito — exigi-la
|
|
260
|
+
// quebraria specs antigas num comando que é gatilho de Action. O parser
|
|
261
|
+
// aqui é o mesmo (tolerante) que o `audit` usa, então "não achei" já
|
|
262
|
+
// significa "o audit também não vai achar" — vale avisar sem bloquear.
|
|
263
|
+
if (dependencySection(specContent) == null) {
|
|
264
|
+
warnings.push(
|
|
265
|
+
'⚠️ Não encontrei a seção de Dependências (heading `#`/`##`/`###` começando com ' +
|
|
266
|
+
'"Dependências") — o `audit` não vai enxergar as dependências desta Feature.'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
// Remove trigger label
|
|
@@ -292,6 +305,7 @@ export async function validate({ issueNumber }) {
|
|
|
292
305
|
`✅ **Validação concluída com sucesso!**\n\n` +
|
|
293
306
|
`- [\`${featureRel}/spec.md\`](${featureRel}/spec.md) ✓\n` +
|
|
294
307
|
`- [\`${featureRel}/plan.md\`](${featureRel}/plan.md) ✓\n\n` +
|
|
308
|
+
(warnings.length > 0 ? `${warnings.join('\n')}\n\n` : '') +
|
|
295
309
|
`A Feature está pronta para a decomposição. O próximo passo gera o **rascunho** ` +
|
|
296
310
|
`em \`${featureRel}/decomposition.md\` para revisão (nada é criado ainda):\n` +
|
|
297
311
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:decompose"\n\`\`\``
|
package/src/config.mjs
CHANGED
|
@@ -341,6 +341,13 @@ export const LABEL_DECOMPOSE_APPLY = 'spec-wave:decompose-apply';
|
|
|
341
341
|
// rotina apagava a label da fila e desligava o agente sem aviso.
|
|
342
342
|
export const LABEL_DEV_AGENT = 'spec-wave:dev-agent';
|
|
343
343
|
|
|
344
|
+
// Prefixo das labels de trilha EXTRA do dev-agent (multi-trilha: rodar mais de
|
|
345
|
+
// um daemon em paralelo, um por fila — `spec-wave:dev-agent-b`, `-c`, …). A
|
|
346
|
+
// fila default (`dev-agent`) já é `LABEL_DEV_AGENT`, coberta acima; este
|
|
347
|
+
// prefixo só identifica as trilhas adicionais, declaradas em `devAgent.queues`
|
|
348
|
+
// do .spec-wave.json (ver `queueLabels`).
|
|
349
|
+
export const DEV_AGENT_QUEUE_PREFIX = `${LABEL_DEV_AGENT}-`;
|
|
350
|
+
|
|
344
351
|
// Gatilho e estado do bug.md (RFC-004 §5). O bug.md é o artefato do Bug — leve
|
|
345
352
|
// por decisão: um defeito não gera spec.md + plan.md.
|
|
346
353
|
export const LABEL_BUG = 'spec-wave:bug';
|
|
@@ -447,6 +454,17 @@ export const LABEL_RISK_ACCEPTED = 'spec-wave:risk-accepted';
|
|
|
447
454
|
// a correção — e troca os findings, fazendo as decisões do TL deixarem de casar.
|
|
448
455
|
export const LABEL_CRITIQUE = 'spec-wave:critique';
|
|
449
456
|
|
|
457
|
+
// Rastro verificável do commit de implementação (item 6 do rfc/plano-
|
|
458
|
+
// hardening-agentes-2026-08.md). O ASSUNTO do commit continua livre — quem
|
|
459
|
+
// implementa escreve o que quiser ("feat: story #1533 [spec-wave-agent]" ou
|
|
460
|
+
// "feat(ep3-f3): wizard de aprovação"); os `git trailer` no rodapé é que são o
|
|
461
|
+
// contrato, porque sobrevivem a squash, rebase e reescrita de assunto (e
|
|
462
|
+
// `git log --format='%(trailers:key=...)'` lê sem depender de convenção de
|
|
463
|
+
// prosa). Ver `lib/commit-trailers.mjs`.
|
|
464
|
+
export const TRAILER_STORY = 'Spec-Wave-Story';
|
|
465
|
+
export const TRAILER_TASKS = 'Spec-Wave-Tasks';
|
|
466
|
+
export const TRAILER_AGENT = 'Spec-Wave-Agent';
|
|
467
|
+
|
|
450
468
|
export const TRIGGER_LABELS = [
|
|
451
469
|
{ name: LABEL_SPEC, color: 'BFD4F2', description: 'Gerar spec.md via GitHub Action' },
|
|
452
470
|
{ name: LABEL_PLAN, color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
|
|
@@ -511,15 +529,50 @@ export function modelLabels(modelAliases) {
|
|
|
511
529
|
}
|
|
512
530
|
|
|
513
531
|
/**
|
|
514
|
-
* Labels
|
|
532
|
+
* Labels das trilhas EXTRA do dev-agent, derivadas de `devAgent.queues`
|
|
533
|
+
* (função PURA) — item 7 do rfc/plano-hardening-agentes-2026-08.md.
|
|
534
|
+
*
|
|
535
|
+
* Mesma armadilha de `modelLabels`, mesmo remédio: sem isto, uma fila custom
|
|
536
|
+
* (`spec-wave:dev-agent-b`, para rodar um segundo daemon em paralelo) é a
|
|
537
|
+
* ÚNICA label que não cabe em `ALL_LABELS` — e o `update` REMOVE toda
|
|
538
|
+
* `spec-wave:*` que não esteja na lista conhecida. Apagar a label a remove de
|
|
539
|
+
* todas as issues: a fila inteira esvazia e o paralelismo morre em silêncio.
|
|
540
|
+
*
|
|
541
|
+
* A fila default (`dev-agent`) fica de fora do retorno — já está coberta por
|
|
542
|
+
* `LABEL_DEV_AGENT`/`ALL_LABELS`, e repeti-la aqui arriscaria duas descrições
|
|
543
|
+
* divergindo da mesma label entre execuções.
|
|
544
|
+
*
|
|
545
|
+
* @param {object} [devAgent] bloco `devAgent` do .spec-wave.json
|
|
546
|
+
* @returns {Array<{name: string, color: string, description: string}>}
|
|
547
|
+
*/
|
|
548
|
+
export function queueLabels(devAgent) {
|
|
549
|
+
const queues = Array.isArray(devAgent?.queues) ? devAgent.queues : [];
|
|
550
|
+
const extra = new Set(
|
|
551
|
+
queues
|
|
552
|
+
.filter(q => typeof q === 'string' && q.trim())
|
|
553
|
+
.map(q => q.trim())
|
|
554
|
+
.filter(q => q !== 'dev-agent')
|
|
555
|
+
);
|
|
556
|
+
return [...extra].map(q => ({
|
|
557
|
+
name: `spec-wave:${q}`,
|
|
558
|
+
color: '5319E7',
|
|
559
|
+
description: `Enfileira a issue para a trilha "${q}" do dev-agent`,
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Labels do fluxo + as de modelo + as de trilha do dev-agent deste repo
|
|
565
|
+
* (função PURA).
|
|
515
566
|
*
|
|
516
567
|
* É este o conjunto que `update`, `doctor` e `init` devem usar — ALL_LABELS
|
|
517
|
-
* sozinha é incompleta em qualquer repo que configure `ai.modelAliases
|
|
568
|
+
* sozinha é incompleta em qualquer repo que configure `ai.modelAliases` ou
|
|
569
|
+
* `devAgent.queues`.
|
|
518
570
|
*
|
|
519
571
|
* @param {object} [fileAi] bloco `ai` do .spec-wave.json
|
|
572
|
+
* @param {object} [devAgent] bloco `devAgent` do .spec-wave.json
|
|
520
573
|
*/
|
|
521
|
-
export function allLabelsFor(fileAi) {
|
|
522
|
-
return [...ALL_LABELS, ...modelLabels(fileAi?.modelAliases)];
|
|
574
|
+
export function allLabelsFor(fileAi, devAgent) {
|
|
575
|
+
return [...ALL_LABELS, ...modelLabels(fileAi?.modelAliases), ...queueLabels(devAgent)];
|
|
523
576
|
}
|
|
524
577
|
|
|
525
578
|
/**
|
package/src/lib/bug-context.mjs
CHANGED
|
@@ -23,11 +23,12 @@ import { STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, PROGRESS_IN_PROGRESS } from '../c
|
|
|
23
23
|
* @param {string|null} [params.codeDigest]
|
|
24
24
|
* @param {string[]} [params.blockedByWarnings]
|
|
25
25
|
* @param {string|null} [params.severity] P0–P3
|
|
26
|
+
* @param {'agent'|'pr-stack'} [params.mode] quem abre o PR — ver lib/delivery-mode.mjs
|
|
26
27
|
* @returns {string}
|
|
27
28
|
*/
|
|
28
29
|
export function buildBugContext({
|
|
29
30
|
bug, bugDoc = null, parent = null, comments = [], codeDigest = null,
|
|
30
|
-
blockedByWarnings = [], severity = null,
|
|
31
|
+
blockedByWarnings = [], severity = null, mode = 'pr-stack',
|
|
31
32
|
}) {
|
|
32
33
|
const lines = [];
|
|
33
34
|
lines.push(`# Contexto de correção — Bug #${bug.number}`);
|
|
@@ -102,11 +103,21 @@ export function buildBugContext({
|
|
|
102
103
|
`1. Commit com a **causa raiz** na mensagem: \`fix: <o que estava errado> (#${bug.number})\`, ` +
|
|
103
104
|
'e o corpo explicando a origem — não o sintoma.'
|
|
104
105
|
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
if (mode === 'agent') {
|
|
107
|
+
// O daemon mantém UM branch e UM PR por issue e abre o PR ele mesmo ao
|
|
108
|
+
// final (inclusive para Bug) — o modelo só faz push no branch atual.
|
|
109
|
+
lines.push('2. Faça **push no branch atual** — **NÃO abra Pull Request**: o agente abre o PR ele mesmo ao final.');
|
|
110
|
+
lines.push(
|
|
111
|
+
`3. O board move sozinho: o Bug sai de **${STAGE_DEVELOPMENT}** (${PROGRESS_IN_PROGRESS}) ` +
|
|
112
|
+
`para **${STAGE_CODE_REVIEW}** quando o PR abre. Não mova à mão.`
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(`2. Abra o Pull Request com \`Fixes #${bug.number}\` no corpo.`);
|
|
116
|
+
lines.push(
|
|
117
|
+
`3. O board move sozinho: o Bug sai de **${STAGE_DEVELOPMENT}** (${PROGRESS_IN_PROGRESS}) ` +
|
|
118
|
+
`para **${STAGE_CODE_REVIEW}** quando o PR abre. Não mova à mão.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
110
121
|
|
|
111
122
|
if (bugDoc && bugDoc.trim()) {
|
|
112
123
|
lines.push('');
|