@spec-wave/cli 0.16.1 → 0.16.4
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/commands/doctor.mjs +46 -0
- package/src/commands/generate-bug.mjs +6 -1
- package/src/commands/generate-plan.mjs +6 -1
- package/src/commands/generate-spec.mjs +6 -1
- package/src/commands/implement.mjs +30 -4
- package/src/commands/refresh.mjs +9 -1
- package/src/commands/update.mjs +1 -1
- package/src/lib/doc-availability.mjs +52 -0
- package/src/lib/unwrap-doc.mjs +64 -0
- package/src/plugin/.claude-plugin/plugin.json +1 -1
package/package.json
CHANGED
package/src/commands/doctor.mjs
CHANGED
|
@@ -282,6 +282,34 @@ async function checkConfig(ctx) {
|
|
|
282
282
|
* @param {string[]} [params.repoLabels] nomes das labels do repo
|
|
283
283
|
* @returns {{ unknownStages: string[], missingStages: string[], orphanLabels: string[], missingLabels: string[] }}
|
|
284
284
|
*/
|
|
285
|
+
/**
|
|
286
|
+
* Os ids de opção do .spec-wave.json batem com os do Project? (função PURA)
|
|
287
|
+
*
|
|
288
|
+
* Compara ID, não nome. `inspectBoardHygiene` compara nomes — e é justamente
|
|
289
|
+
* essa cegueira que deixou passar dois defeitos: o `refresh --stages` que
|
|
290
|
+
* recriava as opções, e o config commitado defasado.
|
|
291
|
+
*
|
|
292
|
+
* Por que importa: o que liga um item do board à coluna é o id. Um
|
|
293
|
+
* `.spec-wave.json` com ids velhos faz TODA escrita de Etapa falhar com "The
|
|
294
|
+
* single select option Id does not belong to the field" — e a falha é
|
|
295
|
+
* best-effort em quase todo caminho (implement, code-review, qa), então o card
|
|
296
|
+
* simplesmente não se move e ninguém percebe.
|
|
297
|
+
*
|
|
298
|
+
* Acontece sempre que alguém roda `refresh --config` e não commita: quem clona
|
|
299
|
+
* o repo — o dev-agent, o runner do Action — lê a versão velha.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} params
|
|
302
|
+
* @param {Record<string,string>|null} [params.configOptions] nome → id do config
|
|
303
|
+
* @param {Record<string,string>|null} [params.boardOptions] nome → id do Project
|
|
304
|
+
* @returns {{ staleIds: string[], checked: boolean }}
|
|
305
|
+
*/
|
|
306
|
+
export function inspectStageIds({ configOptions = null, boardOptions = null } = {}) {
|
|
307
|
+
if (!configOptions || !boardOptions) return { staleIds: [], checked: false };
|
|
308
|
+
const staleIds = Object.keys(configOptions)
|
|
309
|
+
.filter(name => boardOptions[name] && boardOptions[name] !== configOptions[name]);
|
|
310
|
+
return { staleIds, checked: true };
|
|
311
|
+
}
|
|
312
|
+
|
|
285
313
|
export function inspectBoardHygiene({ boardStages = null, repoLabels = null } = {}) {
|
|
286
314
|
const canonical = STATUS_OPTIONS.map(s => s.name);
|
|
287
315
|
const known = new Set(canonical);
|
|
@@ -396,6 +424,24 @@ async function checkBoardHygiene(ctx) {
|
|
|
396
424
|
'do campo "Etapa" e rode `refresh --config`.'
|
|
397
425
|
);
|
|
398
426
|
}
|
|
427
|
+
// Divergência de ID entre o config e o Project: invisível para quem só
|
|
428
|
+
// compara nomes, e fatal para toda escrita de Etapa.
|
|
429
|
+
const { staleIds, checked } = inspectStageIds({
|
|
430
|
+
configOptions: cfg.project.fields?.['Etapa']?.options ?? null,
|
|
431
|
+
boardOptions: snapshot?.fields?.['Etapa']?.options ?? null,
|
|
432
|
+
});
|
|
433
|
+
if (checked && staleIds.length > 0) {
|
|
434
|
+
status = 'warn';
|
|
435
|
+
notes.push(
|
|
436
|
+
`${CONFIG_FILE} com ids de opção DEFASADOS (${staleIds.length} de ` +
|
|
437
|
+
`${Object.keys(cfg.project.fields['Etapa'].options).length}): ${staleIds.join(', ')}. ` +
|
|
438
|
+
'Toda escrita de Etapa falha com "The single select option Id does not belong to the ' +
|
|
439
|
+
'field" — e a falha é best-effort, então o card não se move e nada é reportado. ' +
|
|
440
|
+
`Rode \`refresh --config\` e **commite** o ${CONFIG_FILE}: quem clona o repo ` +
|
|
441
|
+
'(dev-agent, runner do Action) lê a versão commitada.'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
399
445
|
const { missingTrackStages } = inspectBugTrack({ boardStages, openBugCount });
|
|
400
446
|
if (missingTrackStages.length > 0) {
|
|
401
447
|
status = 'warn';
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getIssue, removeLabel, addLabel, commentOnIssue, listIssueComments,
|
|
16
16
|
} from '../api/github-rest.mjs';
|
|
17
17
|
import { generateDocument } from '../lib/claude.mjs';
|
|
18
|
+
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
18
19
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
19
20
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
20
21
|
import { loadPrompt, toolFreeSystemPrompt } from '../lib/prompt-loader.mjs';
|
|
@@ -103,7 +104,7 @@ export async function generateBug({ issueNumber }) {
|
|
|
103
104
|
try {
|
|
104
105
|
console.log(`Gerando bug.md para: ${issue.title}`);
|
|
105
106
|
const systemPrompt = toolFreeSystemPrompt(loadPrompt('bug', { cwd: root }));
|
|
106
|
-
const { content } = await generateDocument(systemPrompt, userContent, {
|
|
107
|
+
const { content: bruto } = await generateDocument(systemPrompt, userContent, {
|
|
107
108
|
action: 'bug',
|
|
108
109
|
labels: issueLabels,
|
|
109
110
|
lint: { lang: TARGET_LANGUAGE },
|
|
@@ -111,6 +112,10 @@ export async function generateBug({ issueNumber }) {
|
|
|
111
112
|
usage: usageEntries,
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
// O modelo às vezes entrega o documento embrulhado em prosa e cerca —
|
|
116
|
+
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
117
|
+
const content = unwrapGeneratedDoc(bruto);
|
|
118
|
+
|
|
114
119
|
mkdirSync(dirAbs, { recursive: true });
|
|
115
120
|
writeFileSync(fileAbs, content, 'utf-8');
|
|
116
121
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
LABEL_CRITIQUE_FAILED, LABEL_NEEDS_HUMAN, DEFAULT_MAX_CRITIQUE_ATTEMPTS, labelNames,
|
|
11
11
|
} from '../config.mjs';
|
|
12
12
|
import { generateDocument } from '../lib/claude.mjs';
|
|
13
|
+
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
13
14
|
import {
|
|
14
15
|
runCritique, resolveCritiqueAttempt, renderNeedsHumanComment,
|
|
15
16
|
} from '../lib/critique.mjs';
|
|
@@ -165,7 +166,7 @@ export async function generatePlan({ issueNumber }) {
|
|
|
165
166
|
try {
|
|
166
167
|
console.log(`Gerando plan.md para: ${issue.title}`);
|
|
167
168
|
const systemPrompt = toolFreeSystemPrompt(loadPrompt('plan', { cwd: root }));
|
|
168
|
-
const { content, lintFindings } = await generateDocument(systemPrompt, userContent, {
|
|
169
|
+
const { content: bruto, lintFindings } = await generateDocument(systemPrompt, userContent, {
|
|
169
170
|
action: 'plan',
|
|
170
171
|
labels: issueLabels,
|
|
171
172
|
lint: { lang: TARGET_LANGUAGE },
|
|
@@ -173,6 +174,10 @@ export async function generatePlan({ issueNumber }) {
|
|
|
173
174
|
usage: usageEntries,
|
|
174
175
|
});
|
|
175
176
|
|
|
177
|
+
// O modelo às vezes entrega o documento embrulhado em prosa e cerca —
|
|
178
|
+
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
179
|
+
const content = unwrapGeneratedDoc(bruto);
|
|
180
|
+
|
|
176
181
|
const published = commitGenerated({
|
|
177
182
|
filePath,
|
|
178
183
|
content,
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { resolveToken } from '../api/auth.mjs';
|
|
3
3
|
import { getIssue, removeLabel, commentOnIssue } from '../api/github-rest.mjs';
|
|
4
4
|
import { generateDocument } from '../lib/claude.mjs';
|
|
5
|
+
import { unwrapGeneratedDoc } from '../lib/unwrap-doc.mjs';
|
|
5
6
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
6
7
|
import { slugify } from '../lib/slugify.mjs';
|
|
7
8
|
import { resolveFromRoot } from '../lib/project-root.mjs';
|
|
@@ -79,7 +80,7 @@ export async function generateSpec({ issueNumber }) {
|
|
|
79
80
|
try {
|
|
80
81
|
console.log(`Gerando spec.md para: ${issue.title}`);
|
|
81
82
|
const systemPrompt = toolFreeSystemPrompt(loadPrompt('spec', { cwd: root }));
|
|
82
|
-
const { content, lintFindings } = await generateDocument(systemPrompt, userContent, {
|
|
83
|
+
const { content: bruto, lintFindings } = await generateDocument(systemPrompt, userContent, {
|
|
83
84
|
action: 'spec',
|
|
84
85
|
labels: issueLabels,
|
|
85
86
|
lint: { lang: TARGET_LANGUAGE },
|
|
@@ -87,6 +88,10 @@ export async function generateSpec({ issueNumber }) {
|
|
|
87
88
|
usage: usageEntries,
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
// O modelo às vezes entrega o documento embrulhado em prosa e cerca —
|
|
92
|
+
// ver lib/unwrap-doc.mjs. O que vai para o repositório é o documento.
|
|
93
|
+
const content = unwrapGeneratedDoc(bruto);
|
|
94
|
+
|
|
90
95
|
const published = commitGenerated({
|
|
91
96
|
filePath,
|
|
92
97
|
content,
|
|
@@ -8,10 +8,11 @@ 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
10
|
} from '../config.mjs';
|
|
11
|
-
import { getIssue, listIssueComments, listBlockedBy } from '../api/github-rest.mjs';
|
|
11
|
+
import { getIssue, listIssueComments, listBlockedBy, getFileContent } from '../api/github-rest.mjs';
|
|
12
12
|
import { listSubIssues, getIssueParent, addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
13
13
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
14
14
|
import { bugDocPaths } from '../lib/bug-doc.mjs';
|
|
15
|
+
import { missingDocMessage, existsOnRemote } from '../lib/doc-availability.mjs';
|
|
15
16
|
import { buildBugContext } from '../lib/bug-context.mjs';
|
|
16
17
|
import { slugify } from '../lib/slugify.mjs';
|
|
17
18
|
import { parseDependencies, orderStories, formatDependencyLine } from '../lib/dependencies.mjs';
|
|
@@ -393,7 +394,14 @@ async function implementBug({ token, owner, repo, config, bug, dryRun, repoRoot
|
|
|
393
394
|
bugDoc = readFileSync(fileAbs, 'utf-8');
|
|
394
395
|
p.log.info(`bug.md encontrado em ${chalk.cyan(fileRel)}.`);
|
|
395
396
|
} else {
|
|
396
|
-
|
|
397
|
+
const onRemote = await existsOnRemote({
|
|
398
|
+
getFileContent, token, owner, repo, pathRel: fileRel,
|
|
399
|
+
});
|
|
400
|
+
p.log.warn(missingDocMessage({
|
|
401
|
+
pathRel: fileRel,
|
|
402
|
+
onRemote,
|
|
403
|
+
fallback: 'o contexto assume a investigação inteira.',
|
|
404
|
+
}));
|
|
397
405
|
}
|
|
398
406
|
|
|
399
407
|
// Board: Bug → Desenvolvimento (In Progress). Não move a Feature-pai.
|
|
@@ -564,7 +572,15 @@ async function implementFeature({ token, owner, repo, config, feature, featureDi
|
|
|
564
572
|
if (existsSync(featureDir)) {
|
|
565
573
|
specPlan = readSpecPlan(featureDir);
|
|
566
574
|
} else {
|
|
567
|
-
|
|
575
|
+
const specRel = `docs/features/${slugify(feature.title)}/spec.md`;
|
|
576
|
+
const onRemote = await existsOnRemote({
|
|
577
|
+
getFileContent, token, owner, repo, pathRel: specRel,
|
|
578
|
+
});
|
|
579
|
+
p.log.warn(missingDocMessage({
|
|
580
|
+
pathRel: specRel,
|
|
581
|
+
onRemote,
|
|
582
|
+
fallback: 'seguindo só com as Stories.',
|
|
583
|
+
}));
|
|
568
584
|
}
|
|
569
585
|
|
|
570
586
|
// F7. Dependências EXTERNAS ainda abertas (da Feature e das Stories pendentes)
|
|
@@ -754,7 +770,17 @@ export async function implement({ issue: issueArg, featureDir: featureDirOpt, dr
|
|
|
754
770
|
if (featureDir && existsSync(featureDir)) {
|
|
755
771
|
specPlan = readSpecPlan(featureDir);
|
|
756
772
|
} else if (featureDir) {
|
|
757
|
-
|
|
773
|
+
const specRel = feature?.title
|
|
774
|
+
? `docs/features/${slugify(feature.title)}/spec.md`
|
|
775
|
+
: featureDir;
|
|
776
|
+
const onRemote = feature?.title
|
|
777
|
+
? await existsOnRemote({ getFileContent, token, owner, repo, pathRel: specRel })
|
|
778
|
+
: null;
|
|
779
|
+
p.log.warn(missingDocMessage({
|
|
780
|
+
pathRel: specRel,
|
|
781
|
+
onRemote,
|
|
782
|
+
fallback: 'seguindo só com as tasks.',
|
|
783
|
+
}));
|
|
758
784
|
} else {
|
|
759
785
|
p.log.warn('Não foi possível resolver a Feature; seguindo só com as tasks (use --feature-dir).');
|
|
760
786
|
}
|
package/src/commands/refresh.mjs
CHANGED
|
@@ -279,5 +279,13 @@ export async function refresh(options = {}) {
|
|
|
279
279
|
`${chalk.dim('Versão CLI:')} ${pkg.version}`,
|
|
280
280
|
'Configuração atualizada'
|
|
281
281
|
);
|
|
282
|
-
|
|
282
|
+
// O commit não é opcional: quem CLONA o repo — o dev-agent e o runner do
|
|
283
|
+
// Action — lê a versão commitada, não a local. Um config não commitado após
|
|
284
|
+
// mudança de board faz toda escrita de Etapa falhar em silêncio para eles.
|
|
285
|
+
p.log.warn(
|
|
286
|
+
`Commite o ${CONFIG_FILE} agora: ele guarda os IDS das opções do board, e quem ` +
|
|
287
|
+
'clona o repositório (dev-agent, GitHub Actions) lê a versão commitada. Sem o ' +
|
|
288
|
+
'commit, os cards deixam de se mover para eles — sem erro visível.'
|
|
289
|
+
);
|
|
290
|
+
p.outro(`${CONFIG_FILE} atualizado. git add ${CONFIG_FILE} && git commit -m "chore: sincroniza o board"`);
|
|
283
291
|
}
|
package/src/commands/update.mjs
CHANGED
|
@@ -596,7 +596,7 @@ export async function update(options = {}) {
|
|
|
596
596
|
(skillJobs.length ? ' Recarregue o agente para pegar a skill nova.' : '') +
|
|
597
597
|
(prUrl ? ` Revise e faça o merge do Pull Request: ${prUrl}` : '') +
|
|
598
598
|
(!prMode && repoFiles.length ? ' Arquivos do repo foram commitados no remoto.' : '') +
|
|
599
|
-
(configPending ? `
|
|
599
|
+
(configPending ? ` COMMITE o ${CONFIG_FILE} — quem clona o repo (dev-agent, Actions) lê a versão commitada.` : '') +
|
|
600
600
|
(finalConfigDecision.included
|
|
601
601
|
? ` O ${CONFIG_FILE} local ficou igual ao do PR — depois do merge, descarte a cópia ` +
|
|
602
602
|
`local com \`git checkout -- ${CONFIG_FILE}\`.`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Documento ausente LOCALMENTE não é a mesma coisa que documento inexistente.
|
|
2
|
+
//
|
|
3
|
+
// spec.md, plan.md e bug.md são gerados por GitHub Action e commitados no
|
|
4
|
+
// REMOTO. Quem roda `implement` logo depois — que é o caso comum, porque a
|
|
5
|
+
// geração é o passo anterior — tem o arquivo no repositório e não no clone.
|
|
6
|
+
//
|
|
7
|
+
// A mensagem antiga dizia "sem bug.md — o contexto assume a investigação
|
|
8
|
+
// inteira", mandando o executor investigar do zero quando bastava um
|
|
9
|
+
// `git pull`. Encontrado no smoke test do RFC-004.
|
|
10
|
+
//
|
|
11
|
+
// Decisão em função pura, I/O na função imperativa: a mensagem é testável sem
|
|
12
|
+
// rede, e a consulta ao remoto é best-effort (nunca lança, nunca bloqueia).
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mensagem para um documento ausente no clone local (função PURA).
|
|
16
|
+
*
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {string} params.pathRel caminho do documento, relativo à raiz do repo
|
|
19
|
+
* @param {boolean|null} params.onRemote true = existe no remoto; false = não
|
|
20
|
+
* existe; null = não foi possível consultar (sem token/rede)
|
|
21
|
+
* @param {string} params.fallback o que o comando fará sem o documento
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function missingDocMessage({ pathRel, onRemote, fallback }) {
|
|
25
|
+
if (onRemote === true) {
|
|
26
|
+
return (
|
|
27
|
+
`${pathRel} existe no repositório mas NÃO no seu clone — rode \`git pull\` e ` +
|
|
28
|
+
'repita o comando. Seguir agora ignoraria o documento já gerado.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (onRemote === false) {
|
|
32
|
+
return `Sem ${pathRel} no repositório — ${fallback}`;
|
|
33
|
+
}
|
|
34
|
+
return (
|
|
35
|
+
`Sem ${pathRel} no clone local (não foi possível consultar o remoto) — ${fallback} ` +
|
|
36
|
+
'Se o documento já foi gerado, rode `git pull` antes.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* O documento existe no remoto? Best-effort — NUNCA lança.
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<boolean|null>} null quando não deu para consultar
|
|
44
|
+
*/
|
|
45
|
+
export async function existsOnRemote({ getFileContent, token, owner, repo, pathRel }) {
|
|
46
|
+
if (!token || !owner || !repo) return null;
|
|
47
|
+
try {
|
|
48
|
+
return (await getFileContent(token, owner, repo, pathRel)) != null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// O modelo às vezes ENTREGA o documento em vez de SER o documento.
|
|
2
|
+
//
|
|
3
|
+
// Encontrado no smoke test do RFC-004: o bug.md commitado começava com
|
|
4
|
+
// "Investigação concluída. […] segue o conteúdo completo do bug.md para ser
|
|
5
|
+
// salvo na raiz do repositório:", trazia o documento dentro de uma cerca
|
|
6
|
+
// ```markdown, e terminava com "Resumo do que foi feito: …".
|
|
7
|
+
//
|
|
8
|
+
// Passou pelo `validate` porque a checagem é `content.includes('# Seção')` —
|
|
9
|
+
// e as seções estavam lá, dentro da cerca. O arquivo versionado ficou com
|
|
10
|
+
// texto conversacional que ninguém pediu, e o `implement` entrega isso ao
|
|
11
|
+
// executor como se fosse o documento.
|
|
12
|
+
//
|
|
13
|
+
// Não dá para resolver só no prompt: pedir "responda apenas com o markdown"
|
|
14
|
+
// já está lá, e o modelo desobedece quando acha que precisa explicar algo.
|
|
15
|
+
// Este módulo trata a saída, que é o único ponto sob nosso controle.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extrai o documento de uma resposta que pode vir embrulhada (função PURA).
|
|
19
|
+
*
|
|
20
|
+
* Dois formatos tratados, nesta ordem:
|
|
21
|
+
* 1. **Cerca envolvente** — o documento inteiro dentro de ```markdown … ```,
|
|
22
|
+
* com preâmbulo antes e/ou epílogo depois. Só é tratada como envolvente a
|
|
23
|
+
* cerca que aparece ANTES do primeiro heading: uma cerca depois do
|
|
24
|
+
* primeiro heading é bloco de código legítimo DO documento.
|
|
25
|
+
* 2. **Preâmbulo solto** — texto antes do primeiro heading, sem cerca.
|
|
26
|
+
*
|
|
27
|
+
* Conteúdo já limpo passa intacto.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} raw resposta do modelo
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function unwrapGeneratedDoc(raw) {
|
|
33
|
+
const text = String(raw ?? '').trim();
|
|
34
|
+
if (!text) return text;
|
|
35
|
+
|
|
36
|
+
const lines = text.split('\n');
|
|
37
|
+
const abreCerca = /^\s*```+\s*(markdown|md)?\s*$/i;
|
|
38
|
+
const fechaCerca = /^\s*```+\s*$/;
|
|
39
|
+
const heading = /^\s{0,3}#{1,6}\s+\S/;
|
|
40
|
+
|
|
41
|
+
const primeiroHeading = lines.findIndex(l => heading.test(l));
|
|
42
|
+
const primeiraCerca = lines.findIndex(l => abreCerca.test(l));
|
|
43
|
+
|
|
44
|
+
// (1) Cerca envolvente: abre antes de qualquer heading e fecha depois.
|
|
45
|
+
if (primeiraCerca !== -1 && (primeiroHeading === -1 || primeiraCerca < primeiroHeading)) {
|
|
46
|
+
// A cerca de fechamento é a ÚLTIMA do texto — cercas internas pertencem ao
|
|
47
|
+
// documento (blocos de código nas seções) e não podem encerrar o embrulho.
|
|
48
|
+
let ultimaCerca = -1;
|
|
49
|
+
for (let i = lines.length - 1; i > primeiraCerca; i -= 1) {
|
|
50
|
+
if (fechaCerca.test(lines[i])) { ultimaCerca = i; break; }
|
|
51
|
+
}
|
|
52
|
+
if (ultimaCerca > primeiraCerca) {
|
|
53
|
+
const dentro = lines.slice(primeiraCerca + 1, ultimaCerca).join('\n').trim();
|
|
54
|
+
if (dentro) return dentro;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// (2) Preâmbulo sem cerca: começa no primeiro heading.
|
|
59
|
+
if (primeiroHeading > 0) {
|
|
60
|
+
return lines.slice(primeiroHeading).join('\n').trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.4",
|
|
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",
|