@spec-wave/cli 0.15.0 → 0.16.1

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.
Files changed (78) hide show
  1. package/README.md +1 -0
  2. package/bin/spec-wave.mjs +44 -5
  3. package/package.json +8 -2
  4. package/src/agent/anthropic-agent.mjs +337 -0
  5. package/src/agent/errors.mjs +33 -0
  6. package/src/agent/index.mjs +108 -0
  7. package/src/agent/openrouter-agent.mjs +378 -0
  8. package/src/agent/run-types.mjs +59 -0
  9. package/src/agent/telemetry.mjs +54 -0
  10. package/src/agent/tools.mjs +452 -0
  11. package/src/agent/tracing.mjs +106 -0
  12. package/src/api/github-graphql.mjs +23 -1
  13. package/src/api/github-rest.mjs +8 -0
  14. package/src/commands/bug.mjs +8 -0
  15. package/src/commands/code-review.mjs +45 -4
  16. package/src/commands/decompose.mjs +22 -72
  17. package/src/commands/dev-agent.mjs +3 -3
  18. package/src/commands/doctor.mjs +77 -6
  19. package/src/commands/generate-bug.mjs +195 -0
  20. package/src/commands/generate-plan.mjs +19 -44
  21. package/src/commands/generate-spec.mjs +18 -46
  22. package/src/commands/implement.mjs +105 -2
  23. package/src/commands/init.mjs +3 -3
  24. package/src/commands/install-skill.mjs +72 -16
  25. package/src/commands/issue.mjs +9 -7
  26. package/src/commands/move.mjs +11 -1
  27. package/src/commands/qa.mjs +23 -2
  28. package/src/commands/refresh.mjs +171 -5
  29. package/src/commands/triage.mjs +174 -0
  30. package/src/commands/update.mjs +16 -3
  31. package/src/commands/validate.mjs +82 -10
  32. package/src/config.mjs +159 -1
  33. package/src/lib/bug-context.mjs +160 -0
  34. package/src/lib/bug-doc.mjs +51 -0
  35. package/src/lib/bug-triage.mjs +81 -0
  36. package/src/lib/claude.mjs +71 -254
  37. package/src/lib/critique.mjs +43 -30
  38. package/src/lib/flow-run.mjs +145 -0
  39. package/src/lib/implement-board.mjs +12 -1
  40. package/src/lib/plugin-skills.mjs +122 -0
  41. package/src/lib/project-root.mjs +9 -2
  42. package/src/lib/prompt-loader.mjs +257 -0
  43. package/src/lib/skill-file.mjs +35 -0
  44. package/src/plugin/.claude-plugin/plugin.json +20 -0
  45. package/src/plugin/README.md +73 -0
  46. package/src/plugin/skills/bug/SKILL.md +60 -0
  47. package/src/plugin/skills/bug/model-prompt.critique.md +48 -0
  48. package/src/plugin/skills/bug/model-prompt.md +74 -0
  49. package/src/plugin/skills/decompose/SKILL.md +117 -0
  50. package/src/plugin/skills/decompose/model-prompt.critique.md +46 -0
  51. package/src/plugin/skills/decompose/model-prompt.feature.md +69 -0
  52. package/src/plugin/skills/decompose/model-prompt.rfc.md +52 -0
  53. package/src/plugin/skills/doctor/SKILL.md +51 -0
  54. package/src/plugin/skills/fix-pr/SKILL.md +130 -0
  55. package/src/plugin/skills/implement/SKILL.md +102 -0
  56. package/src/plugin/skills/info/SKILL.md +40 -0
  57. package/src/plugin/skills/issue/SKILL.md +63 -0
  58. package/src/plugin/skills/move/SKILL.md +52 -0
  59. package/src/plugin/skills/order/SKILL.md +36 -0
  60. package/src/plugin/skills/plan/SKILL.md +58 -0
  61. package/src/plugin/skills/plan/model-prompt.critique.md +44 -0
  62. package/src/plugin/skills/plan/model-prompt.md +59 -0
  63. package/src/plugin/skills/plan/reference/tech-context.md +56 -0
  64. package/src/plugin/skills/ready/SKILL.md +44 -0
  65. package/src/plugin/skills/rfc/SKILL.md +47 -0
  66. package/src/plugin/skills/setup/SKILL.md +67 -0
  67. package/src/plugin/skills/spec/SKILL.md +55 -0
  68. package/src/plugin/skills/spec/model-prompt.md +61 -0
  69. package/src/plugin/skills/story/SKILL.md +49 -0
  70. package/src/plugin/skills/task/SKILL.md +41 -0
  71. package/src/plugin/skills/triage/SKILL.md +52 -0
  72. package/src/plugin/skills/uninstall/SKILL.md +43 -0
  73. package/src/plugin/skills/update/SKILL.md +51 -0
  74. package/src/plugin/skills/workflow/SKILL.md +158 -0
  75. package/src/templates/skill/SKILL.md +54 -4
  76. package/src/templates/workflows/generate-bug.yml +36 -0
  77. package/src/templates/workflows/validate.yml +2 -1
  78. package/src/ui/wizard.mjs +5 -2
@@ -20,6 +20,7 @@ import {
20
20
  * @param {'start'|'success'} phase
21
21
  * @param {{ feature?: {nodeId:string,number:number}|null,
22
22
  * story?: {nodeId:string,number:number}|null,
23
+ * bug?: {nodeId:string,number:number}|null,
23
24
  * tasks?: Array<{nodeId?:string,number:number}> }} refs
24
25
  * @returns {Array<{nodeId:string, label:string, stage:string, status:string,
25
26
  * statusFallback:boolean}>}
@@ -28,7 +29,7 @@ import {
28
29
  * (ex.: Feature já em Desenvolvimento volta a mostrar In Progress).
29
30
  */
30
31
  export function planBoardMoves(phase, {
31
- feature = null, story = null, tasks = [],
32
+ feature = null, story = null, bug = null, tasks = [],
32
33
  // Status das Tasks na fase start: Todo quando enfileiradas atrás de uma
33
34
  // Story; In Progress quando a própria Task é o item sendo implementado.
34
35
  tasksStartStatus = PROGRESS_TODO,
@@ -43,6 +44,12 @@ export function planBoardMoves(phase, {
43
44
  moves.push({ nodeId: story.nodeId, label: `Story #${story.number}`,
44
45
  stage: STAGE_DEVELOPMENT, status: PROGRESS_IN_PROGRESS, statusFallback: true });
45
46
  }
47
+ // Bug é folha e não tem Feature-pai a arrastar: um defeito em correção não
48
+ // deve puxar a Feature inteira de volta para Desenvolvimento.
49
+ if (bug?.nodeId) {
50
+ moves.push({ nodeId: bug.nodeId, label: `Bug #${bug.number}`,
51
+ stage: STAGE_DEVELOPMENT, status: PROGRESS_IN_PROGRESS, statusFallback: true });
52
+ }
46
53
  for (const t of tasks) {
47
54
  if (!t?.nodeId) continue;
48
55
  moves.push({ nodeId: t.nodeId, label: `Task #${t.number}`,
@@ -59,6 +66,10 @@ export function planBoardMoves(phase, {
59
66
  moves.push({ nodeId: story.nodeId, label: `Story #${story.number}`,
60
67
  stage: STAGE_CODE_REVIEW, status: PROGRESS_TODO, statusFallback: false });
61
68
  }
69
+ if (bug?.nodeId) {
70
+ moves.push({ nodeId: bug.nodeId, label: `Bug #${bug.number}`,
71
+ stage: STAGE_CODE_REVIEW, status: PROGRESS_TODO, statusFallback: false });
72
+ }
62
73
  }
63
74
  return moves;
64
75
  }
@@ -0,0 +1,122 @@
1
+ // Enumeração e renderização das skills do PLUGIN — uma skill por comando.
2
+ //
3
+ // Há duas famílias de skill neste pacote, e elas não se misturam:
4
+ //
5
+ // • `src/plugin/skills/<nome>/model-prompt*.md` — textos que a CLI ENVIA a um
6
+ // modelo (ver `prompt-loader.mjs`). Moram DENTRO da skill do comando a que
7
+ // pertencem, mas não são instalados em agente nenhum: dizem o oposto do
8
+ // `SKILL.md` vizinho (um gera a spec.md, o outro proíbe gerá-la à mão).
9
+ // • `src/plugin/skills/<nome>/SKILL.md` — as skills que o USUÁRIO instala no
10
+ // agente de código dele. É o que este módulo enumera.
11
+ //
12
+ // O plugin existe porque o `templates/skill/SKILL.md` monolítico (~800 linhas)
13
+ // entra inteiro no contexto para qualquer pergunta, mesmo trivial. Quebrado em
14
+ // uma skill por comando, o agente carrega só a instrução do comando invocado —
15
+ // e ganha um `/spec-wave:<comando>` por ação em vez de um sub-comando textual.
16
+ //
17
+ // O MESMO diretório serve os dois destinos, porque o formato SKILL.md é comum:
18
+ // • Claude Code → instalado como plugin (`.claude-plugin/plugin.json`), com as
19
+ // skills namespaced em `/spec-wave:<pasta>`;
20
+ // • Codex → copiado para `.agents/skills/<nome>/`, onde não há namespace — daí
21
+ // o `name:` do frontmatter ser `spec-wave-<comando>` e não só `<comando>`.
22
+
23
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ /** Raiz do plugin empacotado (publicado via `"files": ["src"]`). */
30
+ export const PLUGIN_DIR = path.join(__dir, '..', 'plugin');
31
+
32
+ /** Diretório das skills dentro do plugin. */
33
+ export const PLUGIN_SKILLS_DIR = path.join(PLUGIN_DIR, 'skills');
34
+
35
+ // Arquivos que moram na skill mas NÃO são instalados no agente: os prompts que a
36
+ // CLI envia a um modelo. Instalá-los entregaria ao agente de código a instrução
37
+ // contrária à do SKILL.md ao lado — "gere a spec.md" vs. "nunca gere à mão".
38
+ const INSTALL_EXCLUDE_RE = /(^|\/)model-prompt(\.[\w-]+)?\.md$/;
39
+
40
+ /** True se o arquivo da skill deve ir para o agente (função PURA). */
41
+ export function isInstallableSkillFile(relPath) {
42
+ return !INSTALL_EXCLUDE_RE.test(relPath);
43
+ }
44
+
45
+ // Lista os arquivos de um diretório recursivamente, em caminhos relativos a ele
46
+ // e em ordem estável — a ordem entra na comparação de "está atualizado?", então
47
+ // precisa ser determinística entre plataformas.
48
+ function walk(dir, prefix = '') {
49
+ const out = [];
50
+ for (const entry of readdirSync(dir).sort()) {
51
+ const abs = path.join(dir, entry);
52
+ const rel = prefix ? `${prefix}/${entry}` : entry;
53
+ if (statSync(abs).isDirectory()) out.push(...walk(abs, rel));
54
+ else out.push(rel);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Enumera as skills do plugin (função de I/O, mas sem efeitos colaterais).
61
+ *
62
+ * @param {string} [skillsDir]
63
+ * @returns {Array<{ name: string, dir: string, files: string[] }>}
64
+ * `files` são caminhos relativos ao diretório da skill, incluindo o SKILL.md
65
+ * e quaisquer arquivos de apoio (ex.: `reference/tech-context.md`).
66
+ */
67
+ export function listPluginSkills(skillsDir = PLUGIN_SKILLS_DIR) {
68
+ if (!existsSync(skillsDir)) return [];
69
+ return readdirSync(skillsDir)
70
+ .sort()
71
+ .map((name) => ({ name, dir: path.join(skillsDir, name) }))
72
+ .filter((s) => statSync(s.dir).isDirectory() && existsSync(path.join(s.dir, 'SKILL.md')))
73
+ .map((s) => ({ ...s, files: walk(s.dir).filter(isInstallableSkillFile) }));
74
+ }
75
+
76
+ /**
77
+ * Conteúdo desejado de um arquivo de skill no destino (função PURA).
78
+ *
79
+ * Só o `SKILL.md` recebe o banner de versão — arquivos de apoio são copiados
80
+ * verbatim. O banner entra depois do frontmatter, no topo do corpo, que é onde
81
+ * o agente o lê ao carregar a skill.
82
+ *
83
+ * @param {string} relPath caminho relativo dentro da skill
84
+ * @param {string} raw conteúdo original
85
+ * @param {string} version versão da CLI
86
+ * @param {(v: string) => string} banner
87
+ * @returns {string}
88
+ */
89
+ export function renderPluginSkillFile(relPath, raw, version, banner) {
90
+ if (relPath !== 'SKILL.md') return raw;
91
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
92
+ if (!match) return `${banner(version)}\n\n${raw.trim()}\n`;
93
+ return `---\n${match[1]}\n---\n\n${banner(version)}\n\n${match[2].trim()}\n`;
94
+ }
95
+
96
+ /**
97
+ * Plano de gravação de todas as skills do plugin num diretório de destino.
98
+ *
99
+ * Devolve pares `{ path, content }` já renderizados, sem tocar no disco — quem
100
+ * chama decide gravar, comparar (para detectar desatualização) ou só listar
101
+ * (`--dry-run`).
102
+ *
103
+ * @param {string} destDir ex.: `<repo>/.agents/skills`
104
+ * @param {string} version
105
+ * @param {(v: string) => string} banner
106
+ * @param {string} [skillsDir]
107
+ * @returns {Array<{ path: string, content: string, skill: string }>}
108
+ */
109
+ export function planPluginSkillFiles(destDir, version, banner, skillsDir = PLUGIN_SKILLS_DIR) {
110
+ const out = [];
111
+ for (const skill of listPluginSkills(skillsDir)) {
112
+ for (const rel of skill.files) {
113
+ const raw = readFileSync(path.join(skill.dir, rel), 'utf-8');
114
+ out.push({
115
+ skill: skill.name,
116
+ path: path.join(destDir, skill.name, ...rel.split('/')),
117
+ content: renderPluginSkillFile(rel, raw, version, banner),
118
+ });
119
+ }
120
+ }
121
+ return out;
122
+ }
@@ -76,12 +76,19 @@ export function resolveFromRoot(root, ...parts) {
76
76
  *
77
77
  * Era o mesmo bloco copiado em story/task/order/qa/code-review/validate.
78
78
  *
79
+ * `env` é INJETÁVEL, e precisa ser: ler `process.env` aqui dentro tornava
80
+ * impossível testar o caminho local. Quem chamava com `env: {}` para simular
81
+ * "sem GITHUB_REPOSITORY" recebia o valor real assim mesmo — o teste passava na
82
+ * máquina do dev (onde a env não existe) e falhava no GitHub Actions (onde o
83
+ * runner a define).
84
+ *
79
85
  * @param {string} [cwd=process.cwd()]
86
+ * @param {object} [env=process.env] ambiente; injetável para teste
80
87
  * @returns {{ owner: string|undefined, repo: string|undefined,
81
88
  * root: string|null, config: object|null, error: string|null }}
82
89
  */
83
- export function resolveRepoContext(cwd = process.cwd()) {
84
- const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY || '').split('/');
90
+ export function resolveRepoContext(cwd = process.cwd(), env = process.env) {
91
+ const [envOwner, envRepo] = (env.GITHUB_REPOSITORY || '').split('/');
85
92
  const { config, root, error } = loadConfig(cwd);
86
93
  return {
87
94
  owner: envOwner || config?.owner,
@@ -0,0 +1,257 @@
1
+ // Carregador dos prompts de IA — a fonte dos textos que a CLI ENVIA a um modelo.
2
+ //
3
+ // Antes, cada ação carregava seu prompt como template string no próprio comando
4
+ // (`generate-spec.mjs`, `generate-plan.mjs`, `decompose.mjs`, `critique.mjs`),
5
+ // amarrando três coisas que mudam em ritmos diferentes: o texto do prompt, o
6
+ // contrato de saída e o encanamento do GitHub.
7
+ //
8
+ // Hoje os prompts moram DENTRO da skill do comando a que pertencem, como
9
+ // arquivo de apoio ao lado do `SKILL.md`:
10
+ //
11
+ // src/plugin/skills/spec/SKILL.md ← o agente LÊ (dirige a CLI)
12
+ // src/plugin/skills/spec/model-prompt.md ← a CLI ENVIA a um modelo
13
+ // src/plugin/skills/plan/model-prompt.critique.md ← variante `plan/critique`
14
+ //
15
+ // Um diretório por comando, com as duas pontas do handoff lado a lado. Os dois
16
+ // arquivos dizem coisas OPOSTAS de propósito — o `SKILL.md` de `spec` manda
17
+ // nunca gerar a spec.md à mão, e o `model-prompt.md` é justamente quem a gera —
18
+ // e é por isso que continuam sendo arquivos separados em vez de um só.
19
+ //
20
+ // Arquivo de apoio não entra sozinho no contexto de um agente: o Claude Code e o
21
+ // Codex carregam o `SKILL.md`, e só leem o resto se a instrução mandar. Então o
22
+ // `model-prompt.md` fica inerte para quem instala o plugin, e o `install-skill`
23
+ // nem o copia (ver `plugin-skills.mjs`).
24
+ //
25
+ // Precedência: um `.spec-wave/prompts/<id>.md` versionado no repositório do
26
+ // usuário vence o prompt empacotado. É assim que um time ajusta o tom de uma
27
+ // spec, ou aperta o critério da crítica, sem fork da CLI.
28
+
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { parseSkill } from './skill-file.mjs';
33
+
34
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
35
+
36
+ /** Skills empacotadas — os prompts moram dentro delas. */
37
+ export const BUNDLED_SKILLS_DIR = path.join(__dir, '..', 'plugin', 'skills');
38
+
39
+ /** Override por projeto, relativo à raiz do repositório do usuário. */
40
+ export const PROJECT_PROMPTS_DIR = path.join('.spec-wave', 'prompts');
41
+
42
+ /** Prefixo dos arquivos de prompt dentro de uma skill. */
43
+ export const MODEL_PROMPT_PREFIX = 'model-prompt';
44
+
45
+ /**
46
+ * Resolve o id de um prompt no arquivo dentro da skill (função PURA).
47
+ *
48
+ * `spec` → skills/spec/model-prompt.md
49
+ * `plan/critique` → skills/plan/model-prompt.critique.md
50
+ * `decompose/feature`→ skills/decompose/model-prompt.feature.md
51
+ *
52
+ * @param {string} id
53
+ * @returns {{ skill: string, file: string }}
54
+ */
55
+ export function resolvePromptId(id) {
56
+ const [skill, variant] = String(id).split('/');
57
+ if (!skill) throw new Error(`Id de prompt inválido: "${id}".`);
58
+ const file = variant
59
+ ? `${MODEL_PROMPT_PREFIX}.${variant}.md`
60
+ : `${MODEL_PROMPT_PREFIX}.md`;
61
+ return { skill, file };
62
+ }
63
+
64
+ // Somente leitura por padrão. Um prompt que precise escrever declara `tools`
65
+ // explicitamente E recebe `allowedWritePaths` de quem o executa — o motor nega
66
+ // qualquer Write fora dessa lista. Deixar Write no default transformaria um
67
+ // prompt mal calibrado em escrita arbitrária no repo.
68
+ export const DEFAULT_PROMPT_TOOLS = ['Read', 'Glob', 'Grep'];
69
+
70
+ // Teto de turnos. Alto o bastante para explorar um repositório de verdade
71
+ // (Glob → Grep → vários Read) antes de produzir o documento, e baixo o
72
+ // bastante para um loop degenerado morrer em vez de queimar orçamento.
73
+ export const DEFAULT_PROMPT_MAX_TURNS = 30;
74
+
75
+ /**
76
+ * Caminhos candidatos de um prompt, em ordem de precedência (função PURA).
77
+ *
78
+ * @param {string} name nome do prompt (ex.: 'decompose-feature')
79
+ * @param {string} [cwd] raiz do repositório do usuário
80
+ * @returns {Array<{ source: 'project'|'bundled', path: string }>}
81
+ */
82
+ export function promptCandidates(name, cwd = process.cwd()) {
83
+ const { skill, file } = resolvePromptId(name);
84
+ return [
85
+ // Override achatado: `.spec-wave/prompts/plan.critique.md`. Um caminho só,
86
+ // sem recriar a árvore de skills num repositório que não tem plugin nenhum.
87
+ { source: 'project', path: path.join(cwd, PROJECT_PROMPTS_DIR, `${String(name).replace('/', '.')}.md`) },
88
+ { source: 'bundled', path: path.join(BUNDLED_SKILLS_DIR, skill, file) },
89
+ ];
90
+ }
91
+
92
+ // Aceita inteiro positivo vindo de YAML (que pode entregar string ou lixo);
93
+ // qualquer outra coisa cai no default em vez de virar NaN lá na frente.
94
+ function positiveInt(value) {
95
+ const n = Number(value);
96
+ return Number.isInteger(n) && n > 0 ? n : undefined;
97
+ }
98
+
99
+ // Normaliza `tools`: aceita lista YAML ou string separada por vírgula.
100
+ function normalizeTools(value) {
101
+ const raw = Array.isArray(value)
102
+ ? value
103
+ : typeof value === 'string'
104
+ ? value.split(',')
105
+ : null;
106
+ if (!raw) return undefined;
107
+ const tools = raw.map(t => String(t).trim()).filter(Boolean);
108
+ return tools.length > 0 ? tools : undefined;
109
+ }
110
+
111
+ /**
112
+ * Valida e normaliza um prompt já lido do disco (função PURA — testável sem fs).
113
+ *
114
+ * Um corpo vazio é erro, não default: um prompt sem instrução produziria uma
115
+ * chamada de modelo sem propósito, e o custo só apareceria depois.
116
+ *
117
+ * @param {object} params
118
+ * @param {string} params.name
119
+ * @param {object} [params.meta] frontmatter já parseado
120
+ * @param {string} [params.body] corpo markdown (a instrução)
121
+ * @param {'project'|'bundled'} [params.source]
122
+ * @param {string} [params.path]
123
+ * @returns {{ name: string, action: string|null, prompt: string, tools: string[],
124
+ * maxTurns: number, json: { shape: string, retries: number }|null,
125
+ * source: string, path: string, meta: object }}
126
+ */
127
+ export function normalizePrompt({ name, meta = {}, body = '', source = 'bundled', path: filePath = '' } = {}) {
128
+ const prompt = (body || '').trim();
129
+ if (!prompt) {
130
+ throw new Error(
131
+ `Prompt "${name}" está sem corpo (${filePath || 'origem desconhecida'}). ` +
132
+ 'O corpo do model-prompt é a instrução enviada ao modelo — sem ele não há o que executar.'
133
+ );
134
+ }
135
+
136
+ // `json.shape` é o contrato de saída mostrado ao modelo verbatim; quando
137
+ // presente, quem executa exige JSON parseável e re-pede em caso de falha.
138
+ let json = null;
139
+ if (meta.json && typeof meta.json === 'object') {
140
+ const shape = typeof meta.json.shape === 'string' ? meta.json.shape.trim() : '';
141
+ if (!shape) {
142
+ throw new Error(
143
+ `Prompt "${name}" declara \`json\` sem \`shape\` (${filePath}). ` +
144
+ 'Informe o formato esperado ou remova o bloco `json`.'
145
+ );
146
+ }
147
+ json = { shape, retries: positiveInt(meta.json.retries) ?? 1 };
148
+ }
149
+
150
+ return {
151
+ name,
152
+ action: typeof meta.action === 'string' ? meta.action : null,
153
+ prompt,
154
+ tools: normalizeTools(meta.tools) ?? [...DEFAULT_PROMPT_TOOLS],
155
+ maxTurns: positiveInt(meta.maxTurns) ?? DEFAULT_PROMPT_MAX_TURNS,
156
+ json,
157
+ source,
158
+ path: filePath,
159
+ meta,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Encontra e carrega um prompt, respeitando o override do projeto.
165
+ *
166
+ * @param {string} name
167
+ * @param {object} [opts]
168
+ * @param {string} [opts.cwd]
169
+ * @returns {ReturnType<typeof normalizePrompt>}
170
+ */
171
+ export function loadPrompt(name, { cwd = process.cwd() } = {}) {
172
+ const candidates = promptCandidates(name, cwd);
173
+ const found = candidates.find(c => existsSync(c.path));
174
+ if (!found) {
175
+ throw new Error(
176
+ `Prompt "${name}" não encontrado. Procurei em:\n` +
177
+ candidates.map(c => ` - ${c.path} (${c.source})`).join('\n')
178
+ );
179
+ }
180
+ const { meta, body } = parseSkill(readFileSync(found.path, 'utf-8'));
181
+ return normalizePrompt({ name, meta, body, source: found.source, path: found.path });
182
+ }
183
+
184
+ // Delimitadores da orientação que só faz sentido com ferramentas de leitura.
185
+ const REQUIRES_TOOLS_RE = /<!--\s*requires-tools\s*-->[\s\S]*?<!--\s*\/requires-tools\s*-->\n?/g;
186
+
187
+ /**
188
+ * Texto de sistema para um runtime SEM ferramentas (função PURA).
189
+ *
190
+ * `generateDocument`/`generateStructured` fazem UMA chamada de completions: não
191
+ * há loop de tool use, então o modelo não tem `Read`/`Glob`/`Grep` por mais que
192
+ * o frontmatter os declare. Um prompt que afirme possuir ferramentas nesse
193
+ * runtime mente para o modelo — e o resultado típico é uma seção inventada
194
+ * "conforme verifiquei no repositório".
195
+ *
196
+ * Por isso o corpo marca a orientação dependente de ferramentas entre
197
+ * `<!-- requires-tools -->` e `<!-- /requires-tools -->`, e este recorte a
198
+ * remove. O mesmo arquivo serve os dois runtimes sem manter duas versões.
199
+ *
200
+ * @param {ReturnType<typeof normalizePrompt>} prompt
201
+ * @param {string} [appendInstruction] regra que a CLI precisa garantir; vem por
202
+ * ÚLTIMO de propósito — um prompt sobrescrito pelo projeto não pode anulá-la
203
+ * @returns {string}
204
+ */
205
+ export function toolFreeSystemPrompt(prompt, appendInstruction = '') {
206
+ const stripped = prompt.prompt
207
+ .replace(REQUIRES_TOOLS_RE, '')
208
+ .replace(/\n{3,}/g, '\n\n')
209
+ .trim();
210
+ return [stripped, (appendInstruction || '').trim()].filter(Boolean).join('\n\n');
211
+ }
212
+
213
+ /**
214
+ * Converte um prompt em `AgentRunOptions` do `@spec-wave/agent` (função PURA).
215
+ *
216
+ * O corpo vira `systemPromptAppend` — no backend anthropic ele é anexado ao
217
+ * preset `claude_code`, preservando a competência de uso de tools do harness em
218
+ * vez de substituí-la. Aqui o bloco `requires-tools` é PRESERVADO: esse runtime
219
+ * de fato entrega as ferramentas.
220
+ *
221
+ * @param {ReturnType<typeof normalizePrompt>} prompt
222
+ * @param {object} run
223
+ * @param {string} run.model
224
+ * @param {string} run.sessionId
225
+ * @param {string} run.userId
226
+ * @param {boolean} [run.verbose]
227
+ * @param {string[]} [run.allowedWritePaths] quando presente, Write só é
228
+ * permitido nestes caminhos (e `Write` entra na tool surface)
229
+ * @param {string} [run.appendInstruction] instrução controlada pela CLI,
230
+ * anexada DEPOIS do corpo do prompt (ex.: o caminho exato do arquivo de saída)
231
+ * @param {string[]} [run.extraTags] tags extras da trace
232
+ * @returns {object} AgentRunOptions
233
+ */
234
+ export function buildPromptRunOptions(prompt, run) {
235
+ const tools = run.allowedWritePaths?.length && !prompt.tools.includes('Write')
236
+ ? [...prompt.tools, 'Write']
237
+ : [...prompt.tools];
238
+
239
+ const systemPromptAppend = [prompt.prompt, run.appendInstruction]
240
+ .map(part => (part || '').trim())
241
+ .filter(Boolean)
242
+ .join('\n\n');
243
+
244
+ return {
245
+ model: run.model,
246
+ sessionId: run.sessionId,
247
+ userId: run.userId,
248
+ ...(run.verbose === undefined ? {} : { verbose: run.verbose }),
249
+ tools,
250
+ systemPromptAppend,
251
+ maxTurns: prompt.maxTurns,
252
+ ...(run.allowedWritePaths?.length
253
+ ? { allowedWritePaths: [...run.allowedWritePaths] }
254
+ : {}),
255
+ extraTags: ['spec-wave', prompt.name, ...(run.extraTags ?? [])],
256
+ };
257
+ }
@@ -0,0 +1,35 @@
1
+ // Formato de arquivo das skills: frontmatter YAML + corpo markdown.
2
+ //
3
+ // Duas famílias de skill usam o MESMO formato e por isso o mesmo parser:
4
+ // • a skill de orquestração (`src/templates/skill/SKILL.md`), instalada nos
5
+ // agentes de código pelo `install-skill`;
6
+ // • os prompts de IA (`src/plugin/skills/<nome>/model-prompt*.md`), que a CLI
7
+ // envia a um modelo — ver `prompt-loader.mjs`.
8
+ //
9
+ // Vivia dentro de `commands/install-skill.mjs`; foi movido para cá quando o
10
+ // loader passou a precisar dele, para não haver duas implementações do mesmo
11
+ // recorte de frontmatter.
12
+
13
+ import yaml from 'js-yaml';
14
+
15
+ /**
16
+ * Separa o frontmatter YAML do corpo do markdown (função PURA — testável).
17
+ *
18
+ * Tolerante por contrato: arquivo sem frontmatter devolve `meta` vazio e o
19
+ * texto inteiro como corpo; YAML inválido devolve `meta` vazio em vez de
20
+ * lançar. Quem precisa de um campo obrigatório valida depois.
21
+ *
22
+ * @param {string} raw conteúdo do arquivo
23
+ * @returns {{ meta: object, frontmatter: string, body: string }}
24
+ */
25
+ export function parseSkill(raw) {
26
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
27
+ if (!match) return { meta: {}, frontmatter: '', body: raw.trim() };
28
+ let meta = {};
29
+ try {
30
+ meta = yaml.load(match[1]) || {};
31
+ } catch {
32
+ meta = {};
33
+ }
34
+ return { meta, frontmatter: match[1], body: match[2].trim() };
35
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "spec-wave",
3
+ "displayName": "Spec Wave",
4
+ "version": "0.16.1",
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
+ "author": {
7
+ "name": "Astratech",
8
+ "url": "https://github.com/astratech-net-br"
9
+ },
10
+ "homepage": "https://github.com/astratech-net-br/spec-wave-cli",
11
+ "repository": "https://github.com/astratech-net-br/spec-wave-cli",
12
+ "keywords": [
13
+ "spec-driven",
14
+ "github-projects",
15
+ "kanban",
16
+ "rfc",
17
+ "spec-kit",
18
+ "workflow"
19
+ ]
20
+ }
@@ -0,0 +1,73 @@
1
+ # Plugin spec-wave
2
+
3
+ Uma skill por comando do fluxo spec-driven (RFC-001). Em vez de um `SKILL.md`
4
+ monolítico que entra inteiro no contexto para qualquer pergunta, cada comando
5
+ carrega só a sua instrução.
6
+
7
+ ## Skills
8
+
9
+ | Skill | Para quê |
10
+ |-------|----------|
11
+ | `workflow` | Mapa do processo: Kanban, labels, crítica adversarial, roteamento |
12
+ | `setup` | Configura o repositório (`init`) |
13
+ | `info` | Status de configuração |
14
+ | `doctor` | Preflight de auth, escopos, IA, board e workflows |
15
+ | `update` | Atualiza skill, `.spec-wave.json` e workflows/labels |
16
+ | `issue` | Cria Initiative/Epic/Feature/Bug/Spike/RFC no board |
17
+ | `spec` | Gera `spec.md` (1º documento) |
18
+ | `plan` | Gera `plan.md` (2º documento) + `tech_context.yml` |
19
+ | `ready` | Valida spec + plan |
20
+ | `decompose` | Rascunho revisável → Stories/Tasks |
21
+ | `order` | Ordem topológica das Stories |
22
+ | `implement` | Etapa 🚧 Desenvolvimento |
23
+ | `task` | `start` / `done` de uma Task |
24
+ | `story` | `review` de uma Story |
25
+ | `move` | Move qualquer item do board |
26
+ | `rfc` | Escreve um RFC e registra no board |
27
+ | `fix-pr` | Audita e corrige um Pull Request |
28
+ | `uninstall` | Remove a configuração do repositório |
29
+
30
+ ## Instalação
31
+
32
+ ### Claude Code
33
+
34
+ ```
35
+ /plugin marketplace add astratech-net-br/spec-wave-cli
36
+ /plugin install spec-wave@spec-wave
37
+ ```
38
+
39
+ As skills ficam namespaced: `/spec-wave:spec`, `/spec-wave:decompose`, etc.
40
+
41
+ ### Codex
42
+
43
+ Codex não tem plugins — ele varre diretórios de skills. A CLI copia o mesmo
44
+ conteúdo para lá:
45
+
46
+ ```bash
47
+ npx @spec-wave/cli@latest install-skill --agent codex # .agents/skills (projeto)
48
+ npx @spec-wave/cli@latest install-skill --agent codex --global # ~/.agents/skills (usuário)
49
+ ```
50
+
51
+ Como não há namespace nesse destino, o `name:` do frontmatter é
52
+ `spec-wave-<comando>` — invocação: `$spec-wave-spec`.
53
+
54
+ ## Estrutura de uma skill
55
+
56
+ ```
57
+ skills/plan/
58
+ SKILL.md ← o agente LÊ (dirige a CLI)
59
+ model-prompt.md ← a CLI ENVIA a um modelo (id: `plan`)
60
+ model-prompt.critique.md ← idem (id: `plan/critique`)
61
+ reference/tech-context.md ← apoio, lido sob demanda
62
+ ```
63
+
64
+ Os `model-prompt*.md` **não** são instalados em agente nenhum: dizem o oposto do
65
+ `SKILL.md` ao lado. Arquivo de apoio também não entra sozinho no contexto, então
66
+ a cópia do marketplace fica inerte.
67
+
68
+ ## Manutenção
69
+
70
+ - Edite os `SKILL.md` aqui; é a fonte única para os dois destinos.
71
+ - `.claude-plugin/plugin.json` deve acompanhar a versão do `package.json` —
72
+ há um teste que falha se divergirem.
73
+ - Valide com `claude plugin validate ./packages/spec-wave/src/plugin`.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: spec-wave-bug
3
+ description: "Use para gerar o bug.md de um defeito do spec-wave — reprodução, causa raiz, escopo do fix e teste de regressão. Aplica a label spec-wave:bug e deixa o GitHub Action gerar e commitar o arquivo em docs/bugs/<slug>/. Gatilhos: 'gerar o bug.md da issue 42', 'documentar a causa raiz do bug', 'rodar o spec-wave:bug'. Só vale para issues do tipo Bug — Feature usa spec/plan."
4
+ allowed-tools:
5
+ - Bash(gh issue *)
6
+ - Bash(npx @spec-wave/cli@latest *)
7
+ - Read
8
+ ---
9
+
10
+ # spec-wave bug — o documento do defeito
11
+
12
+ > **Regra fundamental: nunca escreva o `bug.md` você mesmo.** Aplique a label e deixe o Action gerar — é isso que garante que o arquivo seja commitado e referenciado na issue. Exceção: revisar/melhorar um `bug.md` já gerado (aí sim use Edit no arquivo local).
13
+
14
+ **Contexto:** leia `.spec-wave.json` (Read). Ausente → skill **setup**.
15
+
16
+ ## Por que existe
17
+
18
+ Um Bug **não** gera `spec.md` + `plan.md`. Esses documentos pedem visão geral funcional, critérios de aceite, requisitos não-funcionais e plano de rollback — peso desproporcional para um defeito.
19
+
20
+ O `bug.md` tem seis seções e uma finalidade: permitir que outra pessoa (ou o dev-agent) **reproduza, entenda e corrija** o defeito, com prova de que corrigiu.
21
+
22
+ ## Passos
23
+
24
+ 1. **Confirme que a issue é do tipo Bug.**
25
+
26
+ > **Apenas Bug.** Para qualquer outro tipo o Action **pula** a geração, remove a label e comenta.
27
+
28
+ 2. Adicione a label de gatilho:
29
+ ```bash
30
+ gh issue edit <número> --add-label "spec-wave:bug"
31
+ ```
32
+
33
+ 3. Informe ao usuário: "Label `spec-wave:bug` adicionada. O Action `generate-bug.yml` vai gerar o `bug.md` automaticamente. Acompanhe em Actions → Generate Bug."
34
+
35
+ 4. Quando concluir, ofereça revisar `docs/bugs/<slug>/bug.md`. O slug vem do título: `[BUG] Duplicidade de pedidos no PIX` → `duplicidade-de-pedidos-no-pix`.
36
+
37
+ Concentre a revisão em **duas seções**: **Causa Raiz** e **Teste de Regressão**. São elas que decidem se a correção ataca o defeito ou o sintoma — o resto do documento é contexto.
38
+
39
+ 5. **Validar:** aplique `spec-wave:ready`. O Action confere as seis seções obrigatórias e aplica `spec-wave:bug-approved`.
40
+
41
+ ## Quando é obrigatório
42
+
43
+ | Severidade | `bug.md` |
44
+ |---|---|
45
+ | **P0** | Opcional — o fix não espera documento. Documente depois, se valer. |
46
+ | **P1** | Recomendado. |
47
+ | **P2 / P3** | **Obrigatório** antes de o bug entrar na fila técnica (✅ Ready). |
48
+
49
+ ## As seis seções
50
+
51
+ `Reprodução` · `Esperado e Obtido` · `Impacto e Severidade` · `Causa Raiz` · `Escopo do Fix` · `Teste de Regressão`
52
+
53
+ O validador procura estes títulos byte a byte — se alguém renomear uma seção ao editar o arquivo, o `spec-wave:ready` reprova.
54
+
55
+ ## Se falhar
56
+
57
+ - **Comentário 🔎 de crítica com `spec-wave:critique-failed`** → a crítica adversarial achou problema grave. Os três alvos mais comuns: a causa raiz não explica todos os sintomas relatados; o escopo do fix é maior que a causa (refatoração pegando carona); o teste de regressão passaria mesmo sem o fix. Corrija o `bug.md`, commite, remova a label e reaplique `spec-wave:bug`.
58
+ - **`spec-wave:needs-human`** → a crítica reprovou N vezes seguidas e o fluxo está **parado**. Uma pessoa precisa revisar e remover a label.
59
+ - **Relato insuficiente** → o `bug.md` sai com "Causa raiz não determinada" e uma lista de hipóteses. Isso é comportamento correto, não falha: leve as perguntas a quem reportou, acrescente as respostas como comentário na issue e reaplique `spec-wave:bug` — os comentários entram no próximo payload.
60
+ - Para reprocessar num modelo mais forte só nesta issue, aplique também `spec-wave:model:<apelido>` (o apelido precisa existir em `ai.modelAliases`).