wizz-method 2.21.0 → 2.22.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 (80) hide show
  1. package/THIRD-PARTY-NOTICES.md +18 -0
  2. package/package.json +4 -2
  3. package/skills-registry.yaml +41 -0
  4. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +1 -1
  5. package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +1 -1
  6. package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
  7. package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +8 -1
  8. package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +2 -2
  9. package/src/bmm-skills/4-implementation/wizz-quick-dev/step-03-implement.md +2 -0
  10. package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +1 -1
  11. package/src/core-skills/wizz-party-mode/customize.toml +3 -3
  12. package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +2 -0
  13. package/src/modules/wizz/_shared/cerebro.md +1 -1
  14. package/src/modules/wizz/_shared/organizacao.md +2 -2
  15. package/src/modules/wizz/agents/wizz-copy/SKILL.md +1 -0
  16. package/src/modules/wizz/agents/wizz-copy/customize.toml +5 -0
  17. package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +2 -1
  18. package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +14 -0
  19. package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +12 -0
  20. package/src/skills-lib/wizz-cerebro/SKILL.md +18 -5
  21. package/src/skills-lib/wizz-design-needs/SKILL.md +7 -0
  22. package/src/skills-lib/wizz-design-needs/references/design-state.md +34 -2
  23. package/src/skills-lib/wizz-design-needs/references/gap-analysis.md +1 -1
  24. package/src/skills-lib/wizz-design-needs/scripts/design_state.py +305 -0
  25. package/src/skills-lib/wizz-design-needs/scripts/tests/test_design_state.py +143 -0
  26. package/src/skills-lib/wizz-impeccable/references/design-rules.md +18 -0
  27. package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +2 -0
  28. package/src/skills-lib/wizz-paid-ads/SKILL.md +2 -1
  29. package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +6 -2
  30. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +7 -34
  31. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +2 -0
  32. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/design-sources.yaml +252 -0
  33. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +1 -1
  34. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mining-rubric.md +123 -0
  35. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +10 -4
  36. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/web-access-and-evidence.md +1 -1
  37. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/design_sources.py +221 -0
  38. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/tests/test_design_sources.py +130 -0
  39. package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
  40. package/src/skills-lib/wizz-taste-skill/references/anti-slop-tells.md +8 -0
  41. package/src/skills-lib/wizz-ui-component-curator/SKILL.md +8 -7
  42. package/src/skills-lib/wizz-video-effects/LICENSE-motionmaxxing.txt +202 -0
  43. package/src/skills-lib/wizz-video-effects/SKILL.md +5 -2
  44. package/src/skills-lib/wizz-video-effects/assets/chrome/collect_layout.js +113 -0
  45. package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +35 -0
  46. package/src/skills-lib/wizz-video-effects/references/data-contract.md +30 -1
  47. package/src/skills-lib/wizz-video-effects/references/gates.md +3 -1
  48. package/src/skills-lib/wizz-video-effects/scripts/check_chrome.py +376 -0
  49. package/src/skills-lib/wizz-video-effects/scripts/check_dead_frames.py +193 -0
  50. package/src/skills-lib/wizz-video-effects/scripts/config.py +13 -0
  51. package/src/skills-lib/wizz-video-effects/scripts/test_chrome.py +181 -0
  52. package/src/skills-lib/wizz-video-effects/scripts/test_dead_frames.py +171 -0
  53. package/src/skills-lib/wizz-video-styles/SKILL.md +6 -2
  54. package/src/skills-lib/wizz-video-styles/references/motion-laws.md +62 -0
  55. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +1 -0
  56. package/src/skills-lib/wizz-video-styles/scripts/blind_review.py +159 -0
  57. package/src/skills-lib/wizz-video-styles/scripts/taste.mjs +110 -0
  58. package/src/skills-lib/wizz-video-styles/scripts/test_blind_review.py +82 -0
  59. package/src/skills-lib/wizz-vsl/LICENSE +49 -0
  60. package/src/skills-lib/wizz-vsl/SKILL.md +122 -0
  61. package/src/skills-lib/wizz-vsl/references/assembly-qa.md +123 -0
  62. package/src/skills-lib/wizz-vsl/references/blocks-and-leads.md +149 -0
  63. package/src/skills-lib/wizz-vsl/references/diagnosis.md +95 -0
  64. package/src/skills-lib/wizz-vsl/references/intake.md +200 -0
  65. package/src/skills-lib/wizz-vsl/references/mechanisms.md +138 -0
  66. package/src/skills-lib/wizz-vsl/references/niches.md +40 -0
  67. package/src/skills-lib/wizz-vsl/references/offer.md +91 -0
  68. package/src/skills-lib/wizz-vsl/references/production.md +75 -0
  69. package/tools/hooks/README.md +10 -9
  70. package/tools/hooks/wizz-arrumar.js +1 -0
  71. package/tools/hooks/wizz-artifact-guard.js +31 -5
  72. package/tools/hooks/wizz-cerebro-lib.js +36 -13
  73. package/tools/hooks/wizz-feature-lib.js +4 -1
  74. package/tools/hooks/wizz-feature.js +66 -76
  75. package/tools/hooks/wizz-sprint-archive-lib.js +311 -0
  76. package/tools/installer/core/hooks-settings.js +1 -0
  77. package/tools/installer/core/installer.js +6 -0
  78. package/tools/installer/modules/core-seed.js +72 -0
  79. package/tools/installer/modules/mcp-config.js +3 -0
  80. package/tools/installer/ui.js +16 -26
@@ -12,18 +12,19 @@ ela é substituída no próximo sync.
12
12
  | `wizz-artifact-guard.js` | PostToolUse (Write/Edit) | Avisa (nunca bloqueia) artefato escrito fora da subpasta da feature ativa, artefato de feature sem feature ativa, frontmatter faltando em `<tipo>-<slug>.md`, e arquivo solto na raiz fora do `layout:`, com o comando que move. Fail-open sem `_wizz/` ou config. |
13
13
  | `wizz-close-reminder.js` | Stop | Aviso ao usuário (`systemMessage`, nunca `decision: block`) quando a feature ativa está `implementado`/`no-ar` e o checklist de fechamento do `wizz-feature.js` tem pendência (ou está no ar e segue ativa). Sem subprocesso, sai cedo fora de projeto Wizz e com feature em andamento, não repete o mesmo aviso na sessão, fail-open. |
14
14
  | `rtk-rewrite.sh` | PreToolUse (Bash) | Reescreve comandos de shell via `rtk rewrite` (token economy). Fork local do hook gerado pelo instalador upstream do rtk, com 3 patches (auditoria 2026-07-07, C4/RT1; e 2026-09-11, RT-H3): reescrita linha a linha em comando multi-linha (o binário só reescrevia a 1ª linha), timeout interno por linha (`RTK_HOOK_TIMEOUT`, default 3s) que não depende do `timeout`/`gtimeout` do GNU coreutils, e cache do version-guard (`RTK_CACHE_TTL_SECONDS`, default 1h, em `${XDG_CACHE_HOME:-$HOME/.cache}/wizz/rtk-version`) pra não rodar `rtk --version` a cada `Bash` tool call da sessão. Requer `rtk` + `jq` no PATH; fail-open em ambos (e no cache: ilegível/corrompido/expirado recomputa em silêncio). |
15
- | `rtk-rewrite.js` | PreToolUse (Bash) | Versão Node do `rtk-rewrite.sh`, registrada no lugar dele só no Windows (sem bash e jq garantidos lá), desde 2026-10-15. Mesmo contrato: guard de multi-linha, linha a linha, timeout por linha, piso 0.23.0, fail-open. Sem o cache de versão. `node test/test-rtk-hook-node.js` exige a mesma saída dos dois contra o mesmo `rtk` fake. |
15
+ | `rtk-rewrite.js` | PreToolUse (Bash) | Versão Node do `rtk-rewrite.sh`, registrada no lugar dele só no Windows (sem bash e jq garantidos lá), desde 2026-10-15. Mesmo contrato: guard de multi-linha, linha a linha, timeout por linha, piso 0.23.0, fail-open. Sem o cache de versão. `node test/test-rtk-hook-node.js` exige a mesma saída dos dois contra o mesmo `rtk` fake. |
16
16
 
17
17
  Arquivos de apoio (copiados junto, NUNCA registrados em settings.json; o doctor cobra a presença):
18
18
 
19
- | Arquivo | O que é |
20
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `wizz-feature-lib.js` | Funções puras de feature, git, config, pastas de artefato e frontmatter. Usada por todos os hooks acima. |
22
- | `wizz-layout-lib.js` | O que pode ficar na raiz e para onde vai o resto (tipos `codigo`, `scripts`, `dados`, `docs`, `midia`, `arquivo`). |
23
- | `wizz-cerebro-lib.js` | Grava o fechamento no Cérebro (`encerrar --cerebro`): acha o vault e `projetos/<projeto>.md`, atualiza a linha da feature e a `Última sessão` do bloco Estado e põe 1 linha em Sessões (teto 15; as antigas vão para `_index/cerebro-archive.md`, lido só por grep). Idempotente, fail-open. |
24
- | `wizz-debug-lib.js` | Diagnóstico opt-in dos hooks: com `WIZZ_DEBUG=1` cada erro engolido vira 1 linha em stderr e é anexada a `~/.claude/wizz-debug.log` (`WIZZ_DEBUG_FILE` troca o caminho). Sem a variável: zero saída, zero disco. Os hooks seguem fail-open (exit 0). O `wizz doctor` roda cada hook com esse modo ligado (check `hooks-run`). |
25
- | `wizz-feature.js` | CLI do ciclo de vida da feature: `resolver`, `definir`, `artefato`, `decidir`, `status`, `verificar`, `encerrar [--cerebro]`, `listar`. É o passo verificável que router/maestro rodam antes de despachar. |
26
- | `wizz-arrumar.js` | CLI de arrumação: `layout [--gravar]`, `planejar`, `aplicar` (manifesto .tsv + referências + testes antes/depois + `desfazer.sh`), `mover`, `desfazer`. |
19
+ | Arquivo | O que é |
20
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `wizz-feature-lib.js` | Funções puras de feature, git, config, pastas de artefato e frontmatter. Usada por todos os hooks acima. |
22
+ | `wizz-layout-lib.js` | O que pode ficar na raiz e para onde vai o resto (tipos `codigo`, `scripts`, `dados`, `docs`, `midia`, `arquivo`). |
23
+ | `wizz-cerebro-lib.js` | Grava o fechamento no Cérebro (`encerrar --cerebro`): acha o vault e `projetos/<projeto>.md`, atualiza a linha da feature e a `Última sessão` do bloco Estado e põe 1 linha em Sessões (teto 15; as antigas vão para `_index/cerebro-archive.md`, lido só por grep). Idempotente, fail-open. |
24
+ | `wizz-debug-lib.js` | Diagnóstico opt-in dos hooks: com `WIZZ_DEBUG=1` cada erro engolido vira 1 linha em stderr e é anexada a `~/.claude/wizz-debug.log` (`WIZZ_DEBUG_FILE` troca o caminho). Sem a variável: zero saída, zero disco. Os hooks seguem fail-open (exit 0). O `wizz doctor` roda cada hook com esse modo ligado (check `hooks-run`). |
25
+ | `wizz-sprint-archive-lib.js` | Arquivamento do `encerrar`: planeja antes de tocar no disco (mescla com `_arquivo/<slug>/`, o de mesmo nome vai para o lado como `.anterior-<data>`, dono do `sprint-status.yaml` da raiz, links e referências de fora) e aplica com desfazer. |
26
+ | `wizz-feature.js` | CLI do ciclo de vida da feature: `resolver`, `definir`, `artefato`, `decidir`, `status`, `verificar`, `encerrar [--cerebro]`, `listar`. É o passo verificável que router/maestro rodam antes de despachar. |
27
+ | `wizz-arrumar.js` | CLI de arrumação: `layout [--gravar]`, `planejar`, `aplicar` (manifesto .tsv + referências + testes antes/depois + `desfazer.sh`), `mover`, `desfazer`. |
27
28
 
28
29
  Regra completa em `src/modules/wizz/_shared/organizacao.md` (2026-10-02, caso Spider). `session-rules.js` passou a lembrar "nenhuma feature ativa" e "projeto sem layout" dentro de projeto Wizz, e `wizz-router-enforce.js` acrescenta 1 linha de feature por prompt em projeto Wizz.
29
30
 
@@ -524,6 +524,7 @@ module.exports = {
524
524
  isAllowedTestArgv,
525
525
  resolveTestCommand,
526
526
  rewriteRefs,
527
+ walkText,
527
528
  readManifest,
528
529
  planMoves,
529
530
  undo,
@@ -9,6 +9,8 @@
9
9
  // 1. Artefato fora do lugar: escrito na pasta de artefatos mas fora da
10
10
  // subpasta da feature ativa, artefato de feature sem feature ativa, ou
11
11
  // artefato tipado (`spec-x.md`...) com frontmatter faltando/errado.
12
+ // Gravar num sprint arquivado (`_arquivo/<slug>/`) também avisa, e o
13
+ // arquivo solto direto em `_arquivo/` não tem dono (2026-10-24).
12
14
  // 2. Arquivo solto na raiz: fora do `layout:` do config. Sugere o destino e
13
15
  // o comando que move atualizando as referências.
14
16
  //
@@ -73,13 +75,37 @@ function artifactWarning(file, content, state, { frontmatterOnly = false } = {})
73
75
  const slug = state.slug;
74
76
  const msgs = [];
75
77
 
78
+ const archived = parts[0] === lib.ARCHIVE_DIR;
79
+ // Arquivo direto em `_arquivo/` não é de sprint nenhum: o dono não é o nome dele.
80
+ const looseInArchive = archived && parts.length === 2;
81
+ const owner = archived ? (looseInArchive ? '' : parts[1]) : parts[0];
82
+ // O caminho dentro do sprint (com as subpastas), para o "mova para".
83
+ const within = parts.length === 1 ? parts : parts.slice(archived ? 2 : 1);
84
+ const inSprint = (sprint) => path.relative(state.root, path.join(base, sprint, ...within));
85
+
86
+ // Sprint fechado é histórico: avisa em Write e Edit, o sprint não reabre sozinho.
87
+ if (archived && !looseInArchive) {
88
+ msgs.push(
89
+ `SPRINT ARQUIVADO: ${relFile} está no sprint fechado "${owner}". ` +
90
+ (slug
91
+ ? `Trabalho novo entra no sprint aberto (${inSprint(slug)}); `
92
+ : 'Trabalho novo entra num sprint aberto (wizz-feature.js definir <slug>); ') +
93
+ 'mexa no arquivo morto só para corrigir o histórico, a pedido do dono.',
94
+ );
95
+ } else if (looseInArchive && !frontmatterOnly) {
96
+ msgs.push(
97
+ `ARQUIVO SOLTO NO ARQUIVO MORTO: ${relFile} está direto em ${lib.ARCHIVE_DIR}/, fora de qualquer sprint fechado. ` +
98
+ `Mova para ${lib.ARCHIVE_DIR}/<slug>/ do sprint a que ele pertence.`,
99
+ );
100
+ }
101
+
76
102
  // Edit: o lugar já foi avisado quando o arquivo nasceu; só o frontmatter conta.
77
103
  if (!frontmatterOnly && parts.length === 1) {
78
104
  if (lib.PROJECT_WIDE.test(name)) return '';
79
105
  if (slug) {
80
106
  msgs.push(
81
107
  `ARTEFATO FORA DA FEATURE: ${relFile} está na raiz das pastas de artefato, mas a feature ativa é "${slug}". ` +
82
- `Mova para ${path.relative(state.root, path.join(base, slug, name))} agora (raiz é só para artefato de projeto inteiro: arquitetura, contexto).`,
108
+ `Mova para ${inSprint(slug)} agora (raiz é só para artefato de projeto inteiro: arquitetura, contexto).`,
83
109
  );
84
110
  } else if (type) {
85
111
  msgs.push(
@@ -87,16 +113,16 @@ function artifactWarning(file, content, state, { frontmatterOnly = false } = {})
87
113
  `Grave a feature a partir do pedido (node ${HOOK_CMD}/wizz-feature.js definir <slug>) e mova o arquivo para a subpasta dela.`,
88
114
  );
89
115
  }
90
- } else if (!frontmatterOnly && slug && parts[0] !== slug && parts[0] !== lib.ARCHIVE_DIR) {
116
+ } else if (!frontmatterOnly && slug && !archived && parts[0] !== slug) {
91
117
  msgs.push(
92
118
  `ARTEFATO EM OUTRO SPRINT: ${relFile} está em "${parts[0]}", mas o sprint aberto é "${slug}". ` +
93
- `Trabalho novo entra no sprint aberto: mova para ${path.relative(state.root, path.join(base, slug, name))}. Abrir outro sprint só com a escolha do dono (wizz-feature.js definir <slug> --novo).`,
119
+ `Trabalho novo entra no sprint aberto: mova para ${inSprint(slug)}. Abrir outro sprint só com a escolha do dono (wizz-feature.js definir <slug> --novo).`,
94
120
  );
95
121
  }
96
122
 
97
123
  if (type) {
98
- const owner = parts[0] === lib.ARCHIVE_DIR ? parts[1] : parts[0];
99
- const problems = lib.frontmatterProblems(content, parts.length > 1 ? owner : slug);
124
+ const expected = parts.length > 1 ? owner : slug;
125
+ const problems = lib.frontmatterProblems(content, expected);
100
126
  if (problems.length > 0) msgs.push(`FRONTMATTER de ${relFile}: ${problems.join('; ')}.`);
101
127
  }
102
128
  return msgs.join(' ');
@@ -6,9 +6,10 @@
6
6
  // Estado + 1 linha na tabela Sessões" (`_shared/cerebro.md`).
7
7
  //
8
8
  // Regras do protocolo do Cérebro (skill `wizz-cerebro`, comandos-sessao.md):
9
- // - vault: `~/Documents/projects/Obsidian Vault/CEREBRO.md`. O `find ~`
10
- // do protocolo fica de fora (caro demais para uma CLI); quem usa outro
11
- // lugar aponta `WIZZ_CEREBRO_VAULT` ou `cerebro_vault:` no config.
9
+ // - vault: mesma ordem da seção `## Vault` da skill (fonte única):
10
+ // `WIZZ_CEREBRO_VAULT` > `cerebro_vault:` no config > Cérebro por projeto
11
+ // (`<output_folder>/cerebro/`) > `~/Documents/projects/Obsidian Vault/`.
12
+ // O `find ~` do protocolo fica de fora (caro demais para uma CLI).
12
13
  // - estado mora em `projetos/<nome>.md`, bloco `## Estado` no topo.
13
14
  // - `Sessões` com teto de 15 linhas; nunca apagar histórico: as antigas
14
15
  // vão para `_index/cerebro-archive.md`, seção `## Sessões antigas —
@@ -49,14 +50,31 @@ const hasCerebro = (dir) => {
49
50
 
50
51
  const expandHome = (p) => (p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p);
51
52
 
53
+ /** Pasta do Cérebro por projeto: `<output_folder>/cerebro/`, `_wizz-output` sem config. */
54
+ function projectVault(root, configValues = {}) {
55
+ if (!root) return '';
56
+ const raw =
57
+ typeof configValues.output_folder === 'string' && configValues.output_folder.trim()
58
+ ? configValues.output_folder.trim()
59
+ : '_wizz-output';
60
+ const out = raw.replace(/^\{project-root\}[/\\]?/, '');
61
+ return path.join(path.isAbsolute(out) ? out : path.join(root, out), 'cerebro');
62
+ }
63
+
52
64
  /**
53
- * Acha o vault: `WIZZ_CEREBRO_VAULT` > `cerebro_vault` do config > padrão do
54
- * protocolo no HOME. Só vale pasta com CEREBRO.md.
65
+ * Acha o vault: `WIZZ_CEREBRO_VAULT` > `cerebro_vault` do config > Cérebro por
66
+ * projeto em `root` > padrão do protocolo no HOME. Só vale pasta com CEREBRO.md.
55
67
  * @param {Record<string,string>} configValues
68
+ * @param {string} [root] raiz do projeto (habilita o Cérebro por projeto)
56
69
  * @returns {string} caminho do vault ou ''
57
70
  */
58
- function findVault(configValues = {}) {
59
- const candidates = [process.env.WIZZ_CEREBRO_VAULT, configValues.cerebro_vault, path.join(os.homedir(), DEFAULT_VAULT)];
71
+ function findVault(configValues = {}, root = '') {
72
+ const candidates = [
73
+ process.env.WIZZ_CEREBRO_VAULT,
74
+ configValues.cerebro_vault,
75
+ projectVault(root, configValues),
76
+ path.join(os.homedir(), DEFAULT_VAULT),
77
+ ];
60
78
  for (const c of candidates) {
61
79
  if (typeof c === 'string' && c.trim() && hasCerebro(expandHome(c.trim()))) return expandHome(c.trim());
62
80
  }
@@ -200,24 +218,29 @@ function writeArchive(vault, projeto, overflow, header) {
200
218
  /**
201
219
  * Grava o fechamento no arquivo do projeto no Cérebro. Fail-open: qualquer
202
220
  * falha devolve {gravado: false, motivo} sem lançar.
203
- * @param {{root: string, slug: string, status: string, projeto?: string}} input
221
+ * @param {{root: string, slug: string, status: string, projeto?: string, spec?: string}} input
222
+ * `spec`: caminho da spec que o `encerrar` acabou de arquivar (sem ele, adivinha)
204
223
  * @returns {{gravado: boolean, arquivo?: string, motivo?: string}}
205
224
  */
206
- function recordClose({ root, slug, status, projeto }) {
225
+ function recordClose({ root, slug, status, projeto, spec: knownSpec }) {
207
226
  try {
208
227
  const { values } = lib.readConfig(root);
209
- const vault = findVault(values);
228
+ const vault = findVault(values, root);
210
229
  if (!vault) return { gravado: false, motivo: 'vault do Cérebro não encontrado (WIZZ_CEREBRO_VAULT)' };
211
230
  const file = findProjectFile(vault, root, values, projeto);
212
231
  if (!file) return { gravado: false, motivo: `sem projetos/<projeto>.md no vault (use --projeto <nome>)` };
213
232
 
214
233
  const date = lib.today();
215
234
  const dirs = lib.artifactDirs(root);
216
- // O `encerrar` arquiva o sprint antes de chamar aqui: procura nas duas casas.
235
+ // O `encerrar` passa a spec que arquivou. Sem ela, a pasta aberta vem
236
+ // primeiro: num sprint reaberto a cópia de `_arquivo/` é o fechamento
237
+ // ANTERIOR, e o Cérebro gravava o resumo velho.
217
238
  const spec =
218
- [path.join(dirs.planning, lib.ARCHIVE_DIR, slug), path.join(dirs.planning, slug)]
239
+ (knownSpec && fs.existsSync(knownSpec) && knownSpec) ||
240
+ [path.join(dirs.planning, slug), path.join(dirs.planning, lib.ARCHIVE_DIR, slug)]
219
241
  .map((d) => path.join(d, `spec-${slug}.md`))
220
- .find((f) => fs.existsSync(f)) || path.join(dirs.planning, slug, `spec-${slug}.md`);
242
+ .find((f) => fs.existsSync(f)) ||
243
+ path.join(dirs.planning, slug, `spec-${slug}.md`);
221
244
  const specText = fs.existsSync(spec) ? fs.readFileSync(spec, 'utf8') : '';
222
245
  const specRel = path.relative(root, spec);
223
246
  const resumo = implementationSummary(specText);
@@ -399,9 +399,11 @@ const MD_LINK = /\]\((?!(?:[a-z][\w+.-]*:|#|\/))([^)\s#?]+)([^)\s]*)\)/gi;
399
399
  * @param {string} oldDir - pasta antiga do arquivo (absoluta)
400
400
  * @param {string} newDir - pasta nova do arquivo (absoluta)
401
401
  * @param {(abs: string) => string} [mapAbs] - caminho atual de um alvo movido
402
+ * @param {{onlyMoved?: boolean}} [opts] - só toca link cujo alvo `mapAbs` moveu
403
+ * (arquivo que não sai do lugar: o resto dos links fica como o autor escreveu)
402
404
  * @returns {string}
403
405
  */
404
- function rebaseMarkdownLinks(text, oldDir, newDir, mapAbs = (p) => p) {
406
+ function rebaseMarkdownLinks(text, oldDir, newDir, mapAbs = (p) => p, { onlyMoved = false } = {}) {
405
407
  return String(text).replaceAll(MD_LINK, (whole, target, rest) => {
406
408
  let decoded;
407
409
  try {
@@ -411,6 +413,7 @@ function rebaseMarkdownLinks(text, oldDir, newDir, mapAbs = (p) => p) {
411
413
  }
412
414
  const original = path.resolve(oldDir, decoded);
413
415
  const abs = mapAbs(original);
416
+ if (onlyMoved && abs === original) return whole;
414
417
  // Link que já não apontava para nada fica como está: reescrever inventaria outro caminho.
415
418
  if (!fs.existsSync(original) && !fs.existsSync(abs)) return whole;
416
419
  const next = path.relative(newDir, abs).split(path.sep).join('/') || '.';
@@ -16,7 +16,10 @@
16
16
  // status <status> rascunho|aprovado|implementado|no-ar na spec
17
17
  // verificar checklist de fechamento (exit 1 se houver pendência)
18
18
  // encerrar status final na spec, arquiva o sprint em _arquivo/
19
- // (com o sprint-status.yaml da raiz), limpa/troca active_feature
19
+ // (com o sprint-status.yaml da raiz quando é dele; o de
20
+ // mesmo nome de um fechamento anterior vai para o lado;
21
+ // falha no meio desfaz tudo; referências de fora corrigidas
22
+ // ou listadas), limpa/troca active_feature
20
23
  // (recusa com pendência do checklist, salvo --forcar);
21
24
  // com --cerebro grava Estado + linha de Sessões no
22
25
  // projetos/<projeto>.md do Cérebro (--projeto <nome>)
@@ -29,6 +32,7 @@ const fs = require('node:fs');
29
32
  const path = require('node:path');
30
33
  const lib = require('./wizz-feature-lib.js');
31
34
  const cerebro = require('./wizz-cerebro-lib.js');
35
+ const archive = require('./wizz-sprint-archive-lib.js');
32
36
 
33
37
  const ALIASES = {
34
38
  resolve: 'resolver',
@@ -61,6 +65,8 @@ function rel(root, p) {
61
65
  return path.relative(root, p) || '.';
62
66
  }
63
67
 
68
+ const toPosixPath = (p) => p.split(path.sep).join('/');
69
+
64
70
  /** Pasta de cada tipo de artefato, para uma feature. */
65
71
  function artifactPath(root, type, slug, dirs = lib.artifactDirs(root)) {
66
72
  const where = lib.ARTIFACT_TYPES[type];
@@ -146,7 +152,10 @@ function cmdDefinir(state, opts) {
146
152
  const slug = lib.toSlug(raw);
147
153
  if (!slug) return { ok: false, code: 2, line: 'Uso: definir <slug ou nome da feature>. Nada foi gravado.' };
148
154
  const { root } = state;
149
- const previous = state.fromConfig;
155
+ // O sprint aberto é o resolvido (branch > config), não só o do config: com o
156
+ // branch apontando outra feature o config velho recusava a certa, e o vazio
157
+ // deixava abrir uma que o branch esconderia (adendo de 2026-10-19).
158
+ const previous = state.slug;
150
159
  // O sprint aberto manda: assunto novo entra nele. Abrir outro sprint com um
151
160
  // aberto é escolha do dono, nunca do agente (pastas demais na Lucrare, 2026-10-15).
152
161
  if (previous && previous !== slug && !opts.novo) {
@@ -261,72 +270,6 @@ function looseArtifacts(root, dirs) {
261
270
  return out;
262
271
  }
263
272
 
264
- /** Move `src` para `dest`, mesclando quando `dest` já existe (sprint reaberto e fechado de novo). */
265
- function moveInto(src, dest) {
266
- if (!fs.existsSync(dest)) {
267
- fs.mkdirSync(path.dirname(dest), { recursive: true });
268
- fs.renameSync(src, dest);
269
- return;
270
- }
271
- for (const name of fs.readdirSync(src)) moveInto(path.join(src, name), path.join(dest, name));
272
- fs.rmdirSync(src);
273
- }
274
-
275
- function markdownFiles(dir) {
276
- let entries = [];
277
- try {
278
- entries = fs.readdirSync(dir, { withFileTypes: true });
279
- } catch {
280
- return [];
281
- }
282
- return entries.flatMap((e) => {
283
- const abs = path.join(dir, e.name);
284
- if (e.isDirectory()) return markdownFiles(abs);
285
- return e.isFile() && e.name.endsWith('.md') ? [abs] : [];
286
- });
287
- }
288
-
289
- /**
290
- * Sprint fechado vai para o arquivo morto: as duas pastas da feature descem
291
- * para `_arquivo/<slug>/`, e o `sprint-status.yaml` da raiz de implementação
292
- * (o do sprint aberto) vai junto, para o próximo sprint começar limpo.
293
- * @returns {{planning: string, implementation: string, sprintStatus: string}}
294
- */
295
- function archiveSprint(root, slug) {
296
- const dirs = lib.artifactDirs(root);
297
- const out = {};
298
- const moves = [dirs.planning, dirs.implementation].map((base) => ({
299
- src: path.join(base, slug),
300
- dest: path.join(base, lib.ARCHIVE_DIR, slug),
301
- }));
302
- const mapAbs = (abs) => {
303
- const hit = moves.find((m) => abs === m.src || abs.startsWith(m.src + path.sep));
304
- return hit ? hit.dest + abs.slice(hit.src.length) : abs;
305
- };
306
- // Links relativos (`../`) descem um nível junto com o arquivo: reescreve antes de mover.
307
- for (const { src, dest } of moves) {
308
- for (const file of markdownFiles(src)) {
309
- const text = fs.readFileSync(file, 'utf8');
310
- const next = lib.rebaseMarkdownLinks(text, path.dirname(file), path.dirname(mapAbs(file)), mapAbs);
311
- if (next !== text) fs.writeFileSync(file, next);
312
- }
313
- if (fs.existsSync(src)) moveInto(src, dest);
314
- }
315
- out.planning = moves[0].dest;
316
- out.implementation = moves[1].dest;
317
- const status = path.join(dirs.implementation, 'sprint-status.yaml');
318
- out.sprintStatus = '';
319
- if (fs.existsSync(status)) {
320
- const dest = path.join(out.implementation, 'sprint-status.yaml');
321
- fs.mkdirSync(out.implementation, { recursive: true });
322
- if (!fs.existsSync(dest)) {
323
- fs.renameSync(status, dest);
324
- out.sprintStatus = dest;
325
- }
326
- }
327
- return out;
328
- }
329
-
330
273
  /**
331
274
  * Checklist de fechamento. Itens mecânicos são checados; os de julgamento
332
275
  * (Cérebro, memória) voltam como "confirmar" para o agente responder.
@@ -390,9 +333,30 @@ function cmdVerificar(state, opts) {
390
333
  }
391
334
 
392
335
  /** `--cerebro`: grava no vault; fail-open, nunca desfaz o encerramento. */
393
- function cerebroClose(state, slug, status, opts) {
336
+ function cerebroClose(state, slug, status, opts, spec) {
394
337
  const projeto = typeof opts.projeto === 'string' ? opts.projeto : '';
395
- return cerebro.recordClose({ root: state.root, slug, status, projeto });
338
+ return cerebro.recordClose({ root: state.root, slug, status, projeto, spec });
339
+ }
340
+
341
+ /**
342
+ * Sprint fechado vai para o arquivo morto: as duas pastas da feature descem
343
+ * para `_arquivo/<slug>/` (mesclando com um fechamento anterior), com a spec
344
+ * já no status final e as referências corrigidas. Planeja antes, aplica com
345
+ * desfazer: falha no meio deixa o projeto como estava.
346
+ */
347
+ function archiveSprint(state, slug, status) {
348
+ const spec = artifactPath(state.root, 'spec', slug);
349
+ const created = !fs.existsSync(spec) && ensureArtifact(state.root, 'spec', slug).created;
350
+ try {
351
+ const text = touch(lib.setFrontmatterField(fs.readFileSync(spec, 'utf8'), 'status', status));
352
+ const plan = archive.planArchive(state.root, slug, { edits: new Map([[spec, text]]) });
353
+ const { asides } = archive.applyArchive(plan);
354
+ return { plan, asides, spec: plan.moves[0].dest + spec.slice(plan.moves[0].src.length) };
355
+ } catch (error) {
356
+ // A spec criada aqui some junto: nada do encerrar fica para trás.
357
+ if (created && fs.existsSync(spec)) fs.unlinkSync(spec);
358
+ throw error;
359
+ }
396
360
  }
397
361
 
398
362
  function cmdEncerrar(state, opts) {
@@ -400,6 +364,15 @@ function cmdEncerrar(state, opts) {
400
364
  if (!slug) return { ok: false, code: 2, line: 'Nenhuma feature ativa para encerrar.' };
401
365
  const status = typeof opts.status === 'string' ? opts.status.toLowerCase() : 'no-ar';
402
366
  if (!lib.STATUS.includes(status)) return { ok: false, code: 2, line: `Status inválido. Use: ${lib.STATUS.join('|')}.` };
367
+ // Rodar de novo num sprint já fechado não recria a spec nem mexe no arquivo morto.
368
+ if (archive.isArchived(state.root, slug)) {
369
+ if (state.fromConfig === slug) lib.writeConfigValue(state.root, 'active_feature', '');
370
+ return {
371
+ ok: true,
372
+ data: { feature: slug, status, ja_encerrado: true },
373
+ line: `Sprint ${slug} já estava encerrado em ${lib.ARCHIVE_DIR}/${slug}/: nada a fazer.`,
374
+ };
375
+ }
403
376
  // Não fecha com pendência: a checagem roda ANTES de gravar, já com o status final.
404
377
  const pend = checklist(state, slug, { statusOverride: status }).pendentes;
405
378
  if (pend.length > 0 && !opts.forcar) {
@@ -410,9 +383,20 @@ function cmdEncerrar(state, opts) {
410
383
  line: `Não encerrei ${slug}: ${pend.map((p) => p.item + (p.detalhe ? ` (${p.detalhe})` : '')).join('; ')}. Resolva e rode de novo (ou --forcar se o dono aceitar a pendência).`,
411
384
  };
412
385
  }
413
- setSpecStatus(state, slug, status);
414
- const archived = archiveSprint(state.root, slug);
415
- const file = path.join(archived.planning, `spec-${slug}.md`);
386
+ let archived;
387
+ try {
388
+ archived = archiveSprint(state, slug, status);
389
+ } catch (error) {
390
+ const desfeito = error.desfeito === false ? 'O desfazer não fechou: confira o projeto.' : 'Desfiz o que tinha feito: nada mudou.';
391
+ return {
392
+ ok: false,
393
+ code: 1,
394
+ data: { feature: slug, erro: error.message },
395
+ line: `Não encerrei ${slug}: ${error.message}. ${desfeito}`,
396
+ };
397
+ }
398
+ const { plan } = archived;
399
+ const file = archived.spec;
416
400
  const next = lib.toSlug(typeof opts.proxima === 'string' ? opts.proxima : '');
417
401
  if (state.fromConfig === slug || !state.fromConfig) lib.writeConfigValue(state.root, 'active_feature', next);
418
402
  if (next) {
@@ -420,10 +404,14 @@ function cmdEncerrar(state, opts) {
420
404
  for (const d of [dirs.planning, dirs.implementation]) fs.mkdirSync(path.join(d, next), { recursive: true });
421
405
  }
422
406
  const parts = [`Sprint ${slug} encerrado (${status}) em ${lib.today()} e arquivado. Spec: ${rel(state.root, file)}.`];
423
- if (archived.sprintStatus) parts.push(`sprint-status.yaml arquivado junto: o próximo sprint começa limpo.`);
407
+ if (plan.status?.owned) parts.push(`sprint-status.yaml arquivado junto: o próximo sprint começa limpo.`);
408
+ else if (plan.status) parts.push(`O sprint-status.yaml da raiz ficou na raiz (${plan.status.motivo}): mova à mão se for deste sprint.`);
409
+ if (archived.asides.length > 0)
410
+ parts.push(`Fechamento anterior preservado ao lado: ${archived.asides.map((a) => rel(state.root, a)).join(', ')}.`);
411
+ if (plan.externas.length > 0) parts.push(`Ainda citam a pasta antiga do sprint (corrija): ${plan.externas.map(toPosixPath).join(', ')}.`);
424
412
  parts.push(next ? `Sprint aberto agora: ${next}.` : 'Nenhum sprint aberto agora: o próximo pedido de trabalho abre um.');
425
413
  if (state.fromBranch === slug) parts.push(`O branch ${state.git.branch} ainda aponta para ${slug}: volte ao tronco ou troque de branch.`);
426
- const memo = opts.cerebro ? cerebroClose(state, slug, status, opts) : null;
414
+ const memo = opts.cerebro ? cerebroClose(state, slug, status, opts, file) : null;
427
415
  if (memo && memo.gravado) parts.push(`Cérebro atualizado: ${memo.arquivo} (Estado + linha em Sessões).`);
428
416
  else {
429
417
  if (memo) parts.push(`Cérebro não gravado (${memo.motivo}).`);
@@ -435,7 +423,9 @@ function cmdEncerrar(state, opts) {
435
423
  status,
436
424
  proxima: next,
437
425
  pendencias: pend.map((p) => p.item),
438
- arquivo: rel(state.root, archived.planning),
426
+ arquivo: rel(state.root, plan.moves[0].dest),
427
+ referencias_externas: plan.externas,
428
+ preservados: archived.asides.map((a) => rel(state.root, a)),
439
429
  };
440
430
  if (memo) data.cerebro = memo;
441
431
  return { ok: true, data, line: parts.join(' ') };