@spec-wave/cli 0.29.0 → 0.32.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.
Files changed (44) hide show
  1. package/package.json +5 -3
  2. package/protocol/qa-result.v1.json +62 -0
  3. package/protocol/qa-trail-report.v1.json +113 -0
  4. package/src/api/github-graphql.mjs +6 -1
  5. package/src/api/github-rest.mjs +21 -0
  6. package/src/cli.mjs +114 -9
  7. package/src/commands/decompose.mjs +29 -3
  8. package/src/commands/doctor.mjs +183 -3
  9. package/src/commands/generate-qa-plan.mjs +421 -0
  10. package/src/commands/implement.mjs +56 -44
  11. package/src/commands/merge.mjs +43 -14
  12. package/src/commands/order.mjs +350 -96
  13. package/src/commands/qa-lead.mjs +748 -0
  14. package/src/commands/qa-run.mjs +892 -0
  15. package/src/commands/run.mjs +5 -1
  16. package/src/config.mjs +32 -1
  17. package/src/lib/artifact-pr.mjs +2 -0
  18. package/src/lib/artifact-publish.mjs +5 -2
  19. package/src/lib/board.mjs +14 -0
  20. package/src/lib/critique.mjs +38 -9
  21. package/src/lib/decomposition-doc.mjs +5 -1
  22. package/src/lib/dependency-map.mjs +300 -0
  23. package/src/lib/doc-paths.mjs +9 -2
  24. package/src/lib/git-retry.mjs +82 -0
  25. package/src/lib/net-cache.mjs +142 -0
  26. package/src/lib/next-step.mjs +15 -3
  27. package/src/lib/qa-exec.mjs +335 -0
  28. package/src/lib/qa-lead-backend.mjs +213 -0
  29. package/src/lib/qa-lead.mjs +627 -0
  30. package/src/lib/qa-plan-doc.mjs +340 -0
  31. package/src/lib/qa-report.mjs +396 -0
  32. package/src/lib/skill-compose.mjs +234 -0
  33. package/src/lib/story-graph.mjs +256 -0
  34. package/src/plugin/.claude-plugin/plugin.json +1 -1
  35. package/src/plugin/skills/merge/SKILL.md +1 -0
  36. package/src/plugin/skills/order/SKILL.md +21 -5
  37. package/src/plugin/skills/qa/SKILL.md +107 -0
  38. package/src/plugin/skills/qa/model-prompt.critique.md +44 -0
  39. package/src/plugin/skills/qa/model-prompt.md +68 -0
  40. package/src/plugin/skills/qa-executor/SKILL.md +76 -0
  41. package/src/plugin/skills/qa-lead/SKILL.md +89 -0
  42. package/src/templates/skill/SKILL.md +981 -279
  43. package/src/templates/skill/core.md +584 -0
  44. package/src/templates/workflows/generate-qa-plan.yml +64 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.29.0",
3
+ "version": "0.32.0",
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": {
@@ -11,10 +11,12 @@
11
11
  },
12
12
  "files": [
13
13
  "bin",
14
- "src"
14
+ "src",
15
+ "protocol"
15
16
  ],
16
17
  "scripts": {
17
- "test": "node --test test/*.test.mjs"
18
+ "test": "node --test test/*.test.mjs",
19
+ "skill:gen": "node scripts/generate-skill.mjs"
18
20
  },
19
21
  "engines": {
20
22
  "node": ">=20"
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/astratech-net-br/spec-wave-cli/blob/main/packages/spec-wave/protocol/qa-result.v1.json",
4
+ "title": "spec-wave qa result v1",
5
+ "description": "Contrato do arquivo `.spec-wave/qa-result-<n>.json`, escrito pelo agente executor e lido por `spec-wave qa <n>`. Formato ADOTADO da implementação existente do comando `qa` (Tarefa Zero da rfc/spec-qa-lead.md): os campos são `cenario`/`evidencia` — não `n`/`evidence` como a §4.2 da spec propunha — estendidos com `blockedReason` (D-QAL6). `version`, `issue`, `planSha` e `headSha` são aceitos e ignorados pela CLI (a integridade do plano é conferida pela própria CLI, que re-lê o qa-plan.md após a execução).",
6
+ "type": "object",
7
+ "required": ["scenarios"],
8
+ "properties": {
9
+ "version": { "type": "integer", "const": 1 },
10
+ "issue": { "type": "integer", "minimum": 1 },
11
+ "planSha": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
12
+ "headSha": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
13
+ "scenarios": {
14
+ "type": "array",
15
+ "minItems": 1,
16
+ "items": {
17
+ "type": "object",
18
+ "required": ["cenario", "verdict"],
19
+ "properties": {
20
+ "cenario": {
21
+ "type": "integer",
22
+ "minimum": 1,
23
+ "description": "Número POSICIONAL do cenário no escopo da execução."
24
+ },
25
+ "verdict": {
26
+ "enum": ["pass", "fail", "blocked"]
27
+ },
28
+ "evidencia": {
29
+ "type": "string",
30
+ "description": "Evidência bruta (comando, saída, código de status). OBRIGATÓRIA e não vazia em `fail` (vira o bug.md determinístico) e em `blocked` com `blockedReason: outro`."
31
+ },
32
+ "blockedReason": {
33
+ "enum": [
34
+ "ambiente",
35
+ "setup-falhou",
36
+ "massa-de-dados",
37
+ "dependencia-nao-entregue",
38
+ "bloqueado-por-bug",
39
+ "credencial",
40
+ "outro"
41
+ ],
42
+ "description": "OBRIGATÓRIO quando `verdict` é `blocked` (D-QAL6). Proibido nos demais vereditos."
43
+ }
44
+ },
45
+ "allOf": [
46
+ {
47
+ "if": { "properties": { "verdict": { "const": "blocked" } } },
48
+ "then": { "required": ["blockedReason"] }
49
+ },
50
+ {
51
+ "if": { "properties": { "verdict": { "const": "fail" } } },
52
+ "then": { "required": ["evidencia"], "properties": { "evidencia": { "minLength": 1 } } }
53
+ },
54
+ {
55
+ "if": { "properties": { "blockedReason": { "const": "outro" } } },
56
+ "then": { "required": ["evidencia"], "properties": { "evidencia": { "minLength": 1 } } }
57
+ }
58
+ ]
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,113 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/astratech-net-br/spec-wave-cli/blob/main/packages/spec-wave/protocol/qa-trail-report.v1.json",
4
+ "title": "spec-wave qa trail report v1",
5
+ "description": "Contrato do `docs/qa/<slug-milestone>/cycle-<n>/report.json`, escrito por `spec-wave qa-lead run` (rfc/spec-qa-lead.md §4.3). É o que a UI do SpecWave e o `qa-lead report` consomem.",
6
+ "type": "object",
7
+ "required": ["version", "milestone", "cycle", "features", "totals", "verdict"],
8
+ "properties": {
9
+ "version": { "type": "integer", "const": 1 },
10
+ "milestone": {
11
+ "type": "object",
12
+ "required": ["number", "title"],
13
+ "properties": {
14
+ "number": { "type": "integer", "minimum": 1 },
15
+ "title": { "type": "string" }
16
+ }
17
+ },
18
+ "cycle": { "type": "integer", "minimum": 1 },
19
+ "startedAt": { "type": ["string", "null"], "format": "date-time" },
20
+ "finishedAt": { "type": ["string", "null"], "format": "date-time" },
21
+ "preflight": {
22
+ "type": "object",
23
+ "required": ["ok", "checks"],
24
+ "properties": {
25
+ "ok": { "type": "boolean" },
26
+ "checks": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "required": ["name", "ok"],
31
+ "properties": {
32
+ "name": { "type": "string" },
33
+ "ok": { "type": "boolean" },
34
+ "detail": { "type": "string" }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ },
40
+ "features": {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "object",
44
+ "required": ["issue", "title", "verdict", "scenarios"],
45
+ "properties": {
46
+ "issue": { "type": "integer", "minimum": 1 },
47
+ "title": { "type": "string" },
48
+ "verdict": {
49
+ "enum": ["verde", "vermelho", "inconclusivo", "execucao-abortada", "fora-do-ciclo", "ja-aprovada"]
50
+ },
51
+ "scenarios": {
52
+ "type": "object",
53
+ "required": ["pass", "fail", "blocked", "total"],
54
+ "properties": {
55
+ "pass": { "type": "integer", "minimum": 0 },
56
+ "fail": { "type": "integer", "minimum": 0 },
57
+ "blocked": { "type": "integer", "minimum": 0 },
58
+ "total": { "type": "integer", "minimum": 0 }
59
+ }
60
+ },
61
+ "bugsOpened": { "type": "array", "items": { "type": "integer" } },
62
+ "durationSec": { "type": ["integer", "null"], "minimum": 0 }
63
+ }
64
+ }
65
+ },
66
+ "totals": {
67
+ "type": "object",
68
+ "required": ["pass", "fail", "blocked", "naoExecutado", "total"],
69
+ "properties": {
70
+ "pass": { "type": "integer", "minimum": 0 },
71
+ "fail": { "type": "integer", "minimum": 0 },
72
+ "blocked": { "type": "integer", "minimum": 0 },
73
+ "naoExecutado": { "type": "integer", "minimum": 0 },
74
+ "total": { "type": "integer", "minimum": 0 }
75
+ }
76
+ },
77
+ "blockedByReason": {
78
+ "type": "object",
79
+ "propertyNames": {
80
+ "enum": [
81
+ "ambiente",
82
+ "setup-falhou",
83
+ "massa-de-dados",
84
+ "dependencia-nao-entregue",
85
+ "bloqueado-por-bug",
86
+ "credencial",
87
+ "outro"
88
+ ]
89
+ },
90
+ "additionalProperties": { "type": "integer", "minimum": 0 }
91
+ },
92
+ "bugs": {
93
+ "type": "object",
94
+ "required": ["opened", "closedSincePreviousCycle", "openBlocking"],
95
+ "properties": {
96
+ "opened": { "type": "integer", "minimum": 0 },
97
+ "closedSincePreviousCycle": { "type": "integer", "minimum": 0 },
98
+ "openBlocking": { "type": "array", "items": { "type": "integer" } }
99
+ }
100
+ },
101
+ "delta": {
102
+ "type": ["object", "null"],
103
+ "properties": {
104
+ "pass": { "type": "integer" },
105
+ "blocked": { "type": "integer" }
106
+ }
107
+ },
108
+ "verdict": { "enum": ["liberada", "nao-liberada", "ciclo-abortado"] },
109
+ "terminationReason": {
110
+ "enum": ["todas-verdes", "sem-mudanca-de-estado", "teto-de-ciclos", "preflight-falhou", null]
111
+ }
112
+ }
113
+ }
@@ -282,7 +282,7 @@ export async function listProjectItems(token, projectId) {
282
282
  }
283
283
  }
284
284
  }
285
- content { ... on Issue { number title state id } }
285
+ content { ... on Issue { number title state id milestone { number title } } }
286
286
  }
287
287
  }
288
288
  }
@@ -304,6 +304,11 @@ export async function listProjectItems(token, projectId) {
304
304
  state: issue.state,
305
305
  nodeId: issue.id,
306
306
  itemId: node.id,
307
+ // Habilita filtrar o snapshot por milestone sem nenhuma chamada extra
308
+ // (`order --milestone`, e o cache do board fica mais rico de graça).
309
+ milestone: issue.milestone
310
+ ? { number: issue.milestone.number, title: issue.milestone.title }
311
+ : null,
307
312
  fields,
308
313
  });
309
314
  }
@@ -307,6 +307,27 @@ export async function listMilestones(token, owner, repo) {
307
307
  });
308
308
  }
309
309
 
310
+ // Milestone pelo número — o `qa-lead` lê a DESCRIÇÃO atual antes de atualizar
311
+ // o bloco de trilha (substituir o bloco exige conhecer o resto do texto).
312
+ export async function getMilestone(token, owner, repo, milestoneNumber) {
313
+ const octokit = makeOctokit(token);
314
+ const res = await octokit.rest.issues.getMilestone({
315
+ owner, repo, milestone_number: milestoneNumber,
316
+ });
317
+ return res.data;
318
+ }
319
+
320
+ // Atualiza SÓ a descrição do milestone. O chamador é responsável por preservar
321
+ // o que já estava lá (Release Notes etc.) — ver upsertMilestoneBlock: o
322
+ // contrato é substituir um bloco delimitado, nunca a descrição inteira.
323
+ export async function updateMilestoneDescription(token, owner, repo, milestoneNumber, description) {
324
+ const octokit = makeOctokit(token);
325
+ const res = await octokit.rest.issues.updateMilestone({
326
+ owner, repo, milestone_number: milestoneNumber, description,
327
+ });
328
+ return res.data;
329
+ }
330
+
310
331
  // Issues de uma milestone (pelo NÚMERO dela), abertas e fechadas.
311
332
  //
312
333
  // `state: 'all'` de propósito: uma Feature fechada continua contando para o
package/src/cli.mjs CHANGED
@@ -151,7 +151,7 @@ export function buildProgram() {
151
151
  .option('--dry-run', 'Decide e explica sem executar nada')
152
152
  .option('--yes', 'Confirma o passo que exige confirmação')
153
153
  .option('--apply', 'Autoriza especificamente o decompose-apply (erra se o passo pendente for outro)')
154
- .option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug')
154
+ .option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug | qa-plan')
155
155
  .option('--max-steps <n>', 'Encadeia até N passos (padrão: 1)', '1')
156
156
  .option('--force', 'Ignora o portão de label de gatilho pendente')
157
157
  .option('--no-remote-check', 'Não consulta o remoto pelos documentos ausentes (offline)')
@@ -320,12 +320,101 @@ export function buildProgram() {
320
320
 
321
321
  program
322
322
  .command('qa')
323
- .description('Move Feature para QA ao aprovar ou mergear um PR (usado pelo GitHub Action)')
324
- .requiredOption('--pr-number <n>', 'Número do Pull Request')
323
+ .description('Executa o plano de QA de uma issue (Feature/Story/Bug) localmente; com --pr-number, move a Feature para QA (usado pelo GitHub Action)')
324
+ .argument('[issue]', 'Número da issue (Feature, Story ou Bug), ex.: 12 ou #12')
325
+ .option('--pr-number <n>', 'Modo Action: move a Feature/Bug do PR aprovado ou mergeado para 🧪 QA')
326
+ .option('--only <n[,m]>', 'Executa só o(s) cenário(s) indicado(s) — número POSICIONAL no plano')
327
+ .option('--severity <p>', 'Severidade dos Bugs abertos na reprova: P0, P1, P2 ou P3 (default: qa.defaultBugPriority ou P2)')
328
+ .option('--dry-run', 'Monta o contexto e imprime cenários e comando — ZERO escrita no GitHub')
329
+ .action(async (issueArg, options) => {
330
+ // Dois modos, mutuamente exclusivos: `--pr-number` é a transição Code
331
+ // Review → 🧪 QA (Action); o argumento posicional é a EXECUÇÃO do plano.
332
+ if (issueArg && options.prNumber) {
333
+ console.error('Use OU `qa <issue>` (executa o plano) OU `qa --pr-number <n>` (move o board) — nunca os dois.');
334
+ process.exit(1);
335
+ }
336
+ if (!issueArg && !options.prNumber) {
337
+ console.error('Informe a issue (`spec-wave qa <issue>`) ou o PR (`spec-wave qa --pr-number <n>`).');
338
+ process.exit(1);
339
+ }
340
+ if (options.prNumber) {
341
+ const { qa } = await import('./commands/qa.mjs');
342
+ await qa(options).catch(err => { console.error(err.message); process.exit(1); });
343
+ return;
344
+ }
345
+ const { qaRun } = await import('./commands/qa-run.mjs');
346
+ await qaRun({ issue: issueArg, ...options })
347
+ .catch(err => { console.error(err.message); process.exit(1); });
348
+ })
349
+ .addHelpText('after', `
350
+ A fase de QA em duas metades (a ordem é obrigatória):
351
+ 1. plano label spec-wave:qa na FEATURE → Action gera docs/features/<slug>/qa-plan.md
352
+ (valida + critica; limpo → label spec-wave:qa-ready)
353
+ 2. executar qa <issue> — SEMPRE local, contra o checkout. Exige qa-ready na Feature:
354
+ o verde avança a Etapa sozinho, então o portão humano é a revisão do plano.
355
+
356
+ Alvos: qa <feature> (Stories ainda sem qa-approved) · qa <story> · qa <bug> (Teste de Regressão do bug.md).
357
+
358
+ Desfechos:
359
+ verde +qa-approved · Story → 📋 Homologação · Bug → 🚀 Deploy · Feature quando todas liberarem
360
+ (Bug filho aberto segura a Story mesmo verde)
361
+ vermelho 1 Bug por cenário reprovado (filho da Story, ✅ Ready, bug.md commitado) · exit 1
362
+ blocked sem fail = inconclusivo: nada move, nenhum Bug · exit 1
363
+
364
+ Executor: "qa": { "command": "..." } no .spec-wave.json (ou SPEC_WAVE_QA_CMD).
365
+ Placeholders: {contextFile} {qaPlanFile} {specFile} {issue} {type} {title}.
366
+ Re-teste após o fix: qa <story> --only <cenário> (não duplica Bug — comenta no existente).
367
+
368
+ Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
369
+
370
+ program
371
+ .command('qa-lead')
372
+ .description('Orquestra o QA de uma trilha (milestone): prepara os planos (plan), executa o ciclo em containers paralelos (run) e reimprime relatórios (report)')
373
+ .argument('<acao>', 'plan | run | report')
374
+ .argument('[milestone]', 'Milestone da trilha, por número ou título, ex.: 12 ou "v1.4"')
375
+ .option('--watch', 'No plan: aguarda os planos disparados resolverem (qa-ready/critique-failed), com teto qa.lead.planWaitTimeoutMin')
376
+ .option('--only <features>', 'No run: sub-trilha explícita, ex.: --only 318,320 (decisão humana declarada, não relaxamento do portão)')
377
+ .option('--max-cycles <n>', 'No run: teto de ciclos desta trilha (default: qa.lead.maxCycles, 3)')
378
+ .option('--cycle <n>', 'No report: qual ciclo imprimir (default: o último)')
379
+ .option('--dry-run', 'Classifica/planeja e imprime — nenhuma label, zero container, zero escrita')
380
+ .action(async (acao, milestone, options) => {
381
+ const { qaLead } = await import('./commands/qa-lead.mjs');
382
+ await qaLead({ action: acao, milestone, ...options })
383
+ .catch(err => { console.error(err.message); process.exit(1); });
384
+ })
385
+ .addHelpText('after', `
386
+ Trilha = milestone (D-QAL1), na ordem do \`spec-wave order\` sem argumento. Duas fases (D-QAL2):
387
+ plan classifica cada Feature (pronta / portão humano / em voo / sem spec-plan) e aplica
388
+ \`spec-wave:qa\` nas que precisam de plano. PARA no portão humano: revise os qa-plan.md.
389
+ run exige a trilha inteira com \`spec-wave:qa-ready\`. Preflight global → até
390
+ qa.lead.maxParallel containers (um por Feature, backend docker) → coleta os comentários
391
+ de veredito → grava docs/qa/<slug>/cycle-<n>/{report.md,report.json} e o bloco
392
+ delimitado na descrição do milestone. Ciclo N+1 só se um Bug fechou ou um bloqueio
393
+ caiu (D-QAL7); teto de 3 ciclos.
394
+ report reimprime um ciclo já gravado, sem executar nada.
395
+
396
+ O Lead NUNCA aplica label de estado, não move card e não abre issue — isso é do \`qa\` dentro
397
+ dos containers. Configuração: bloco "qa": { "lead": { ... } } no .spec-wave.json (§4.5 da spec);
398
+ \`container.image\` é obrigatória para a fase run.
399
+
400
+ Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
401
+
402
+ program
403
+ .command('generate-qa-plan')
404
+ .description('Gera (ou re-critica) o plano de QA de uma Feature em qa-plan.md — roda no GitHub Action ou localmente')
405
+ .requiredOption('--issue-number <n>', 'Número da issue no GitHub')
325
406
  .action(async (options) => {
326
- const { qa } = await import('./commands/qa.mjs');
327
- await qa(options).catch(err => { console.error(err.message); process.exit(1); });
328
- });
407
+ const { generateQaPlan } = await import('./commands/generate-qa-plan.mjs');
408
+ await generateQaPlan(options).catch(err => { console.error(err.message); process.exit(1); });
409
+ })
410
+ .addHelpText('after', `
411
+ Só em FEATURE (o plano é um arquivo por Feature, com seções "## Cenário N — Story #X").
412
+ Arquivo ausente → gera via IA e publica em PR; presente → valida + critica COMO ESTÁ
413
+ (edições manuais preservadas — para regerar do zero, apague o arquivo e reaplique a label).
414
+ Requer spec.md e plan.md na base. Limpo → spec-wave:qa-ready (revise o plano ANTES de
415
+ rodar \`spec-wave qa <issue>\` — o veredito verde avança a Etapa sozinho).
416
+
417
+ Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
329
418
 
330
419
  program
331
420
  .command('implement')
@@ -333,6 +422,7 @@ export function buildProgram() {
333
422
  .argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
334
423
  .option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
335
424
  .option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
425
+ .option('--refresh', 'Ignora o cache local (sub-issues/dependências) e reconsulta a API')
336
426
  .action(async (issue, options) => {
337
427
  const { implement } = await import('./commands/implement.mjs');
338
428
  await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
@@ -342,10 +432,21 @@ export function buildProgram() {
342
432
  .command('order')
343
433
  .description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
344
434
  .argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
345
- .action(async (feature) => {
435
+ .option('--milestone <ref>', 'Só as Features desta milestone (número ou título) no mapa sem argumento')
436
+ .option('--json', 'Saída em JSON estável (para agentes/scripts), sem ANSI')
437
+ .option('--remote', 'Une também o blocked_by da API (1 chamada por Story) — pega arestas criadas só pela UI')
438
+ .option('--refresh', 'Ignora o cache local e reconsulta a API')
439
+ .option('--sync', 'Grava as dependências VIVAS (body + blocked_by) de volta no decomposition.md e regenera o dependency-map.json')
440
+ .action(async (feature, options) => {
346
441
  const { order } = await import('./commands/order.mjs');
347
- await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
348
- });
442
+ await order({ feature, ...options }).catch(err => { console.error(err.message); process.exit(1); });
443
+ })
444
+ .addHelpText('after', `
445
+ As ARESTAS vêm de fontes locais (dependency-map.json do decompose --apply, decomposition.md,
446
+ linhas "Depende de:" do corpo) — zero chamada por Story. A Etapa vem de UM snapshot do board,
447
+ cacheado por ${'`cache.ttlSec`'} (default 600s; SPEC_WAVE_CACHE_TTL na env; 0 desliga) com
448
+ staleness sempre visível. blocked_by criado SÓ pela UI não está nas fontes locais: use --remote
449
+ para incluí-lo na consulta, ou --sync para gravá-lo em definitivo nos artefatos.`);
349
450
 
350
451
  program
351
452
  .command('merge')
@@ -354,6 +455,8 @@ export function buildProgram() {
354
455
  .option('--yes', 'Executa os merges (sem isso, só mostra o plano)')
355
456
  .option('--dry-run', 'Mostra o plano e sai')
356
457
  .option('--keep-branches', 'Não apaga as branches das Stories depois do merge')
458
+ .option('--remote', 'No plano: une também o blocked_by da API (a execução com --yes sempre o faz, fresco)')
459
+ .option('--refresh', 'Ignora o cache local e reconsulta a API')
357
460
  .action(async (feature, options) => {
358
461
  const { merge } = await import('./commands/merge.mjs');
359
462
  await merge({ feature, ...options }).catch(err => { console.error(err.message); process.exit(1); });
@@ -441,6 +544,8 @@ Fluxo típico (cada tema, na ordem):
441
544
  decompor run <issue> (decompose → --apply) → order <feature>
442
545
  implementar implement <issue> · task start/done · story review
443
546
  entregar merge <feature> (PRs empilhados, na ordem) · run --pr <n> (board até QA)
547
+ validar (QA) label spec-wave:qa (gera qa-plan.md) → qa <issue> [--only n] (executa e dá o veredito)
548
+ trilha inteira (milestone): qa-lead plan → [revisão humana] → qa-lead run · qa-lead report
444
549
  acompanhar info · order · move · repair-stage
445
550
 
446
551
  Docs: https://astratech-net-br.github.io/spec-wave-cli/ · spec-wave <comando> --help`);
@@ -38,6 +38,8 @@ import { detectIssueType } from '../lib/issue-type.mjs';
38
38
  import { loadConfig } from '../lib/project-root.mjs';
39
39
  import { resolveFlowContext } from '../lib/flow-run.mjs';
40
40
  import { publishArtifact } from '../lib/artifact-publish.mjs';
41
+ import { buildDependencyMap, storiesFromAppliedDoc, DEPENDENCY_MAP_FILE } from '../lib/dependency-map.mjs';
42
+ import { invalidateCache } from '../lib/net-cache.mjs';
41
43
  import { loadArtifact } from '../lib/doc-source.mjs';
42
44
  import { awaitingMergeBlock } from '../lib/artifact-pr.mjs';
43
45
  import { isAwaitingMerge } from '../lib/doc-source.mjs';
@@ -258,14 +260,14 @@ function formatItemsLintWarning(texts) {
258
260
  * dezenas de itens). Sem o `await`, a promise rejeitada não é capturada por esse
259
261
  * catch: o aviso some e vira unhandled rejection.
260
262
  */
261
- async function publishFile(ctx, { doc, pathRel, content, nextLabel = null }) {
263
+ async function publishFile(ctx, { doc, pathRel, content, nextLabel = null, extraFiles = [] }) {
262
264
  const { token, owner, repo, issue, issueNumber, base } = ctx;
263
265
  const published = await publishArtifact({
264
266
  token, owner, repo, doc,
265
267
  issueNumber: parseInt(issueNumber, 10),
266
268
  issueTitle: issue?.title || '',
267
269
  issueUrl: issue?.html_url || '',
268
- pathRel, content, base, nextLabel,
270
+ pathRel, content, base, nextLabel, extraFiles,
269
271
  });
270
272
  if (published.warning) console.warn(`⚠️ ${published.warning}`);
271
273
  return published;
@@ -626,10 +628,30 @@ async function applyDecomposition(ctx) {
626
628
  // da branch do rascunho: aquela já foi mergeada (é pré-condição do apply), e
627
629
  // reabri-la produziria um PR reintroduzindo estado antigo.
628
630
  try {
631
+ const appliedAt = new Date().toISOString();
632
+ // O dependency-map.json viaja no MESMO commit da anotação: o apply é o
633
+ // momento em que o grafo nasce pronto em memória — é ele que torna a ordem
634
+ // consultável por qualquer clone SEM pagar a API (lib/dependency-map.mjs).
635
+ const extraFiles = [];
636
+ if (doc.kind === 'stories') {
637
+ const converted = storiesFromAppliedDoc({ ...doc, appliedAt });
638
+ if (converted.ok) {
639
+ const mapRel = docRel.replace(/decomposition\.md$/, DEPENDENCY_MAP_FILE);
640
+ extraFiles.push({
641
+ path: mapRel,
642
+ content: `${JSON.stringify(buildDependencyMap({
643
+ featureNumber: number, stories: converted.stories, source: 'apply', generatedAt: appliedAt,
644
+ }), null, 2)}\n`,
645
+ });
646
+ } else {
647
+ console.warn(`⚠️ dependency-map.json não gerado: ${converted.reason}.`);
648
+ }
649
+ }
629
650
  const anotado = await publishFile(ctx, {
630
651
  doc: 'decomposition-apply',
631
652
  pathRel: docRel,
632
- content: renderDecompositionDoc({ ...doc, appliedAt: new Date().toISOString() }),
653
+ content: renderDecompositionDoc({ ...doc, appliedAt }),
654
+ extraFiles,
633
655
  });
634
656
  console.log(anotado.pr?.number
635
657
  ? `${docRel} anotado com as issues criadas — PR #${anotado.pr.number}.`
@@ -641,6 +663,10 @@ async function applyDecomposition(ctx) {
641
663
  );
642
664
  }
643
665
 
666
+ // As sub-issues da Feature mudaram — o cache local de quem consultar agora
667
+ // mentiria por até um TTL inteiro.
668
+ invalidateCache(ctx.root, `subissues-${number}`, 'board-items');
669
+
644
670
  await addLabel(token, owner, repo, number, LABEL_DECOMPOSED)
645
671
  .catch(err => console.warn(`Falha ao aplicar a label ${LABEL_DECOMPOSED}: ${err.message}`));
646
672
  // Best-effort como as duas vizinhas: as issues já existem, e derrubar o run