wizz-method 2.20.1 → 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 (116) 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-funil/SKILL.md +1 -1
  13. package/src/modules/lowticket/skills/wizz-lowticket-guia/SKILL.md +2 -2
  14. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/SKILL.md +5 -5
  15. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/01-principios.md +1 -1
  16. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/02-diagnostico-funil.md +1 -1
  17. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/03-campanha-bidcap.md +1 -1
  18. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +2 -2
  19. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04a-criativos-ia.md +6 -6
  20. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04b-criativos-formatos.md +1 -1
  21. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04c-criativos-roteiro.md +5 -5
  22. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/05-oferta.md +1 -1
  23. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06-pagina-vendas.md +3 -1
  24. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06a-instagram.md +2 -2
  25. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06b-player-vsl.md +1 -1
  26. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07-funil-upsell.md +1 -1
  27. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07a-copy-humanizacao.md +1 -1
  28. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/08-pesquisa-publico.md +1 -1
  29. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/09-especialista-coproducao.md +1 -1
  30. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/10-levantar-caixa.md +1 -1
  31. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/11-checklist-roi.md +1 -1
  32. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12-mineracao-ofertas.md +1 -1
  33. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12a-mineracao-veredito.md +1 -1
  34. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12b-fabrica-de-ofertas.md +2 -2
  35. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12c-anatomia-low-ticket.md +3 -1
  36. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/13-benchmarks-vsl.md +1 -1
  37. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/14-vocabulario.md +1 -1
  38. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/15-como-estender.md +2 -2
  39. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/17-rastreamento.md +1 -1
  40. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/18-copy-desejo-oferta.md +3 -3
  41. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/INDEX.md +14 -12
  42. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/guardrails.md +18 -70
  43. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +9 -9
  44. package/src/modules/lowticket/skills/wizz-lowticket-minerador/SKILL.md +1 -1
  45. package/src/modules/lowticket/skills/wizz-lowticket-pagina/SKILL.md +1 -1
  46. package/src/modules/lowticket/skills/wizz-lowticket-trafego/SKILL.md +1 -1
  47. package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +2 -0
  48. package/src/modules/wizz/_shared/cerebro.md +1 -1
  49. package/src/modules/wizz/_shared/organizacao.md +2 -2
  50. package/src/modules/wizz/agents/wizz-copy/SKILL.md +1 -0
  51. package/src/modules/wizz/agents/wizz-copy/customize.toml +5 -0
  52. package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +2 -1
  53. package/src/modules/wizz/agents/wizz-growth/knowledge/playbook.md +1 -1
  54. package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +14 -0
  55. package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +12 -0
  56. package/src/skills-lib/wizz-cerebro/SKILL.md +18 -5
  57. package/src/skills-lib/wizz-design-needs/SKILL.md +7 -0
  58. package/src/skills-lib/wizz-design-needs/references/design-state.md +34 -2
  59. package/src/skills-lib/wizz-design-needs/references/gap-analysis.md +1 -1
  60. package/src/skills-lib/wizz-design-needs/scripts/design_state.py +305 -0
  61. package/src/skills-lib/wizz-design-needs/scripts/tests/test_design_state.py +143 -0
  62. package/src/skills-lib/wizz-impeccable/references/design-rules.md +18 -0
  63. package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +2 -0
  64. package/src/skills-lib/wizz-paid-ads/SKILL.md +2 -1
  65. package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +6 -2
  66. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +7 -34
  67. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +2 -0
  68. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/design-sources.yaml +252 -0
  69. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +1 -1
  70. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mining-rubric.md +123 -0
  71. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +10 -4
  72. package/src/skills-lib/wizz-premium-landing-ui-researcher/references/web-access-and-evidence.md +1 -1
  73. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/design_sources.py +221 -0
  74. package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/tests/test_design_sources.py +130 -0
  75. package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
  76. package/src/skills-lib/wizz-taste-skill/references/anti-slop-tells.md +8 -0
  77. package/src/skills-lib/wizz-ui-component-curator/SKILL.md +8 -7
  78. package/src/skills-lib/wizz-video-effects/LICENSE-motionmaxxing.txt +202 -0
  79. package/src/skills-lib/wizz-video-effects/SKILL.md +5 -2
  80. package/src/skills-lib/wizz-video-effects/assets/chrome/collect_layout.js +113 -0
  81. package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +35 -0
  82. package/src/skills-lib/wizz-video-effects/references/data-contract.md +30 -1
  83. package/src/skills-lib/wizz-video-effects/references/gates.md +3 -1
  84. package/src/skills-lib/wizz-video-effects/scripts/check_chrome.py +376 -0
  85. package/src/skills-lib/wizz-video-effects/scripts/check_dead_frames.py +193 -0
  86. package/src/skills-lib/wizz-video-effects/scripts/config.py +13 -0
  87. package/src/skills-lib/wizz-video-effects/scripts/test_chrome.py +181 -0
  88. package/src/skills-lib/wizz-video-effects/scripts/test_dead_frames.py +171 -0
  89. package/src/skills-lib/wizz-video-styles/SKILL.md +6 -2
  90. package/src/skills-lib/wizz-video-styles/references/motion-laws.md +62 -0
  91. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +1 -0
  92. package/src/skills-lib/wizz-video-styles/scripts/blind_review.py +159 -0
  93. package/src/skills-lib/wizz-video-styles/scripts/taste.mjs +110 -0
  94. package/src/skills-lib/wizz-video-styles/scripts/test_blind_review.py +82 -0
  95. package/src/skills-lib/wizz-vsl/LICENSE +49 -0
  96. package/src/skills-lib/wizz-vsl/SKILL.md +122 -0
  97. package/src/skills-lib/wizz-vsl/references/assembly-qa.md +123 -0
  98. package/src/skills-lib/wizz-vsl/references/blocks-and-leads.md +149 -0
  99. package/src/skills-lib/wizz-vsl/references/diagnosis.md +95 -0
  100. package/src/skills-lib/wizz-vsl/references/intake.md +200 -0
  101. package/src/skills-lib/wizz-vsl/references/mechanisms.md +138 -0
  102. package/src/skills-lib/wizz-vsl/references/niches.md +40 -0
  103. package/src/skills-lib/wizz-vsl/references/offer.md +91 -0
  104. package/src/skills-lib/wizz-vsl/references/production.md +75 -0
  105. package/tools/hooks/README.md +10 -9
  106. package/tools/hooks/wizz-arrumar.js +1 -0
  107. package/tools/hooks/wizz-artifact-guard.js +31 -5
  108. package/tools/hooks/wizz-cerebro-lib.js +36 -13
  109. package/tools/hooks/wizz-feature-lib.js +4 -1
  110. package/tools/hooks/wizz-feature.js +66 -76
  111. package/tools/hooks/wizz-sprint-archive-lib.js +311 -0
  112. package/tools/installer/core/hooks-settings.js +1 -0
  113. package/tools/installer/core/installer.js +6 -0
  114. package/tools/installer/modules/core-seed.js +72 -0
  115. package/tools/installer/modules/mcp-config.js +3 -0
  116. package/tools/installer/ui.js +16 -26
@@ -0,0 +1,91 @@
1
+ # Phase 4.1 · The OFFER (written first)
2
+
3
+ > Counterintuitive and non-negotiable: **the offer is the first block written**, even though it is the last the viewer sees. Prerequisite: `manifest.gate2_aprovado_em` filled in. Check it before writing.
4
+ >
5
+ > When an approved `wizz-offer-forge` offer exists, it is the source for steps 2 to 13 (name, stack, bonuses, guarantee, anchor): this block turns it into **spoken** script, it does not redesign it. A change of price, bonus or guarantee goes back to `wizz-offer-forge` and reopens Gate 2.
6
+
7
+ Default position: the pitch enters at the 12 to 14 minute turn and runs 4 to 6 more (full VSL 16 to 20). Under the low ticket ruler, the pitch enters at about two thirds of the runtime the ruler sets.
8
+
9
+ ## The 15 steps, in order
10
+
11
+ **1. Transition from the mechanism to the offer.**
12
+ > "Agora que você entendeu <mecanismo da solução>, deixa eu te mostrar como aplicar isso na sua vida a partir de hoje."
13
+
14
+ Never cut dry to "so I created a course".
15
+
16
+ **2. Product name.** Strong and specific. Words that convey a system (*protocolo, método, sistema, programa, desafio*). Avoid *curso* and *formação*: they sound like content, not result.
17
+
18
+ **3. Who it is for, and who it is not for.**
19
+ > "Isso é pra você se <condição 1>, <condição 2> e <condição 3>. E não é pra você se <condição>."
20
+
21
+ The negative qualification raises perceived value and buyer quality (Block 1.9).
22
+
23
+ **4. What the person receives.** Module by module, always **benefit, not feature**:
24
+ > ❌ "Módulo 3: Alimentação"
25
+ > ✅ "Módulo 3: o que comer nos dias em que a agenda vira do avesso, pra você não recomeçar do zero toda segunda"
26
+
27
+ **5. The triad.** Strong products communicate three layers:
28
+
29
+ | Layer | Line | Example |
30
+ |---|---|---|
31
+ | **Knowledge** | "Eu te ensino a pescar" | the content, straight to the point |
32
+ | **Recommendation** | "Eu te dou o peixe" | what to do in her specific case (diagnosis, plan, AI) |
33
+ | **Follow-up** | "Eu te acompanho na pescaria" | continuous support: group, WhatsApp, AI, team |
34
+
35
+ Name the layers that exist and never invent the missing ones. If two are missing, flag it to the human: it is an offer weakness, not a copy one.
36
+
37
+ **6. Interleaved testimonials.** `[DEPOIMENTO 1]`, `[DEPOIMENTO 2]`… spread **between** modules, never piled in one block. When there is no proof material, the markers hold the place, or the block is omitted; the rest of the pitch goes on.
38
+
39
+ **7. Price anchoring.** Before revealing the price, compare with the real, expensive alternative:
40
+ > "Uma consultoria individual comigo sai R$ X por sessão. Um acompanhamento de 3 meses passa de R$ Y."
41
+
42
+ Anchor only against a real, supported alternative. A charging history or discount that never existed is never stated as fact.
43
+
44
+ **8. Price with contrast** · `[BOTÃO APARECE AQUI]`
45
+ > "Mas hoje não é nada disso. É R$ <preço>."
46
+
47
+ The button appears **here**, when the price is revealed: that is what the player delay waits for.
48
+
49
+ **9. Bonus with a 60-second timer.** A real urgency bonus with a visual countdown, typically a live workshop seat. It is one of the strongest levers of the format. Honesty requirement: the bonus exists and is delivered. A timer that resets and keeps offering destroys trust in the whole funnel.
50
+
51
+ **10. Guarantee.** Period and condition, in one clear sentence that matches the real terms exactly (an encouraged usage challenge is not automatically a refund condition).
52
+
53
+ **11. Extra bonuses, with a story.** Each bonus gets 2 or 3 lines of origin. A bonus listed without story is noise; with story it is value.
54
+
55
+ **12. Express stack.** Quick recap, in a list, with value per item:
56
+ > "Programa completo: R$ X · Bônus 1: R$ Y · Bônus 2: R$ Z · Total: R$ <soma>. Hoje: R$ <preço>."
57
+
58
+ **13. Surprise gift.**
59
+ > "E tem uma coisa que eu nem coloquei na lista: assim que você entrar, recebe um convite pra <aula/encontro>."
60
+
61
+ It raises the open rate of the first post-purchase message, where the whole backend starts.
62
+
63
+ **14. Emotional close.** Go back to the lead's problem and project the two futures.
64
+
65
+ **15. Final CTA.** One instruction, no ambiguity: "Clica no botão aqui embaixo agora." `[CTA → aponta pro botão que já apareceu no passo 8]`
66
+
67
+ ## The value equation (apply to every line of the pitch)
68
+
69
+ Every element of the offer pushes at least one of: **↑ dream outcome · ↑ perceived likelihood · ↓ time to result · ↓ effort and sacrifice**. A bonus or module that moves none of the four weakens the offer by dilution.
70
+
71
+ ## Page and player rules (they move conversion more than copy)
72
+
73
+ These belong to the page around the player, written by `wizz-copywriting`; list them in the hand-off.
74
+
75
+ | Rule | Why |
76
+ |---|---|
77
+ | **No headline above the video** | Removing the headline improves play rate |
78
+ | **No email or phone form before the VSL** | Capturing first lowers front-end conversion |
79
+ | **Delayed button**, only after the price | An early button anticipates the pitch and lowers retention |
80
+ | **No discount button or coupon on the page** | Discount buyers take the upsell far less |
81
+ | Traffic goes straight to the open page | No intermediate capture step |
82
+
83
+ Under the low ticket ruler, its page and player shards (`06b-player-vsl.md`, `12c-anatomia-low-ticket.md`) win over this table.
84
+
85
+ ## Output of this step
86
+
87
+ The offer block in **spoken language** (it is a script, not page text), with `[DEPOIMENTO]`, `[BOTÃO APARECE AQUI]` and `[A DEFINIR]` markers where information is missing.
88
+
89
+ ## OTO and downsell videos
90
+
91
+ A short upsell video after purchase follows a different sequence, in [blocks-and-leads.md](blocks-and-leads.md) §OTO. It never implies that the buyer accepted another optional offer.
@@ -0,0 +1,75 @@
1
+ # Phase 6 · Production plan and hand-off
2
+
3
+ > Animation makes a clear script easier to follow; it cannot rescue vague writing. Production starts only after the script-only audit and the claims ledger are closed ([assembly-qa.md](assembly-qa.md)). This skill **plans** the production and hands it off: drawing, voicing and assembling the video belong to the designer area. The copy agent never invokes another area's agent: offer `wizz-maestro` when the user wants the video in the same delivery.
4
+
5
+ ## 1. Beat sheet · `05-beat-sheet.md`
6
+
7
+ One row per beat, in presentation order. It is the contract the production reads.
8
+
9
+ | # | Narration (exact) | On-screen line | Visual evidence | Claim supported | Source |
10
+ |---|---|---|---|---|---|
11
+ | 1 | "…" | ≤ 6 words | product crop / real screen / `[B-ROLL: …]` | ledger #3 | `00-briefing.md` 7.4 |
12
+
13
+ - Capture product states only after confirming the script and the UI agree. Label fixture or example data honestly. Never expose customer data, credentials or live keys.
14
+ - A small visual system: two type roles, a restrained palette, consistent safe margins, a few repeatable scene types (intro, problem, reveal, demo, proof, guarantee, offer, CTA). Patterns, not required branding: the project's brand wins.
15
+ - No durations by hand: every beat's length comes from the final audio (section 3).
16
+
17
+ ## 2. Narration
18
+
19
+ - Record or generate the narration from the **exact** final script. Paid voice generation only with the user's explicit authorization; keep the complete inputs (text, voice and model settings, request IDs, returned alignment, audio hashes) so an uncertain job is resumed instead of paid twice.
20
+ - When revising, regenerate only the changed sentences if the voice splices cleanly, with adjacent text as prosody context. Listen to every seam; replace the whole paragraph only when the splice stays audible.
21
+
22
+ ## 3. Timing from the final audio
23
+
24
+ Invoke the `wizz-ctc-align` skill on the final narration and the exact spoken text: word-by-word timestamps snapped to real silences become the single timing source. Never estimate timing by hand and never use Whisper timestamps for a known script.
25
+
26
+ - Scene durations are built from that alignment.
27
+ - Big on-screen lines change on **spoken beats**, anchored to the word timings, never to arbitrary seconds.
28
+ - If a section is cut after alignment, update audio, word timings, captions, scene hosts, animation anchors, music ducking and the final fade **together**. Old offsets become invalid after an upstream edit.
29
+
30
+ ## 4. Drawing and assembly (designer area)
31
+
32
+ The method's video pipeline rule: **HyperFrames draws the screens (silent visual), Remotion assembles the film (audio, captions, frame timing).**
33
+
34
+ - `hyperframes` draws each silent scene from the beat sheet: kinetic headline cards, animated stack, price reveal, guarantee card, real product mockups.
35
+ - The `wizz-remotion-best-practices` skill assembles the scenes with the narration, captions, SFX and the motion anchored to the `wizz-ctc-align` timings.
36
+ - Effects and sound by semantic event, if wanted: the `wizz-video-effects` skill, after the assembly plan.
37
+
38
+ ### Motion rules for the big lines
39
+
40
+ - Split on-screen headlines into one to three short lines, each a complete, concrete thought.
41
+ - Enter lines in sequence with fast, damped motion; use scale, opacity or horizontal movement consistently.
42
+ - Highlight one decisive line or word with the accent colour; never colour every benefit.
43
+ - Keep copy readable after its entrance. Motion marks a beat, reveals evidence or guides focus.
44
+ - During demos the UI dominates: crops and pans focus attention without hiding the state needed to verify the claim. When the finished output is the strongest proof, play it unobstructed and long enough to judge.
45
+ - Respect title-safe areas, captions, mobile scaling and reduced motion.
46
+
47
+ **Illustrated VSL (optional reference).** For a slide-by-slide stickman or illustrated VSL with a timed image manifest and frame verification, `mikefilsaime-groove/stickman-vsl-director` (MIT) is a reference for the storyboard grammar (one image per narration span, a mandatory art-direction choice, a review deck before rendering). It is not installed by the method; its own scripts are not run from here.
48
+
49
+ ## 5. Verification of the rendered master
50
+
51
+ - Inspect frames from the rendered master, not only development snapshots.
52
+ - Watch with sound from start to end; listen at every splice and transition.
53
+ - Check captions against the final audio and the visible claims.
54
+ - No black gaps, clipped lines, stale frames, drift, missing fonts or unreadable demo crops.
55
+ - Measure streams, dimensions, runtime, loudness, peaks, file size and hashes when relevant. Keep a high-quality master separate from the web encode.
56
+
57
+ ## 6. Player QA (on the real page)
58
+
59
+ Test the actual page player on phone width and desktop: explicit sound start, restart behaviour, native controls, captions, close or escape, pause when out of viewport, poster, reduced motion, and the button delay set to the price moment (`[BOTÃO APARECE AQUI]`). Under the low ticket ruler, `06b-player-vsl.md` of `wizz-lowticket-metodologia` sets what the player must deliver.
60
+
61
+ Report the measured runtime and the remaining limitations. Nothing is published, deployed, or sent to paid generation without the user's authorization.
62
+
63
+ ## Hand-off block
64
+
65
+ Close Phase 6 with this block in the reply:
66
+
67
+ ```
68
+ 🎬 HANDOFF DE PRODUÇÃO · <produto>
69
+ Roteiro final: {output_folder}/vsl/<slug>/03-vsl-principal.md (lead default: <qual>)
70
+ Beat sheet: {output_folder}/vsl/<slug>/05-beat-sheet.md (<N> beats)
71
+ Ledger de claims: 04-qa.md (<N> claims, <N> com marcador pendente)
72
+ Régua: <default | lowticket: duração e posição do pitch>
73
+ Ordem: narração final → wizz-ctc-align → hyperframes (cenas mudas) → Remotion (montagem) → QA do player
74
+ Página em volta do player: wizz-copywriting com as regras de página e player
75
+ ```
@@ -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(' ') };