@spec-wave/cli 0.16.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,
@@ -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.3",
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",