wizz-method 2.18.0 → 2.19.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 (154) hide show
  1. package/package.json +5 -3
  2. package/removals.txt +10 -0
  3. package/skills-registry.yaml +50 -38
  4. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +9 -9
  5. package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
  6. package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +1 -1
  7. package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/SKILL.md +1 -1
  8. package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/checklist.md +1 -1
  9. package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +1 -1
  10. package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +5 -5
  11. package/src/bmm-skills/module-help.csv +1 -1
  12. package/src/bmm-skills/module.yaml +1 -1
  13. package/src/core-skills/_shared/handoff-protocol.md +2 -2
  14. package/src/modules/lowticket/skills/wizz-lowticket-funil/SKILL.md +1 -1
  15. package/src/modules/lowticket/skills/wizz-lowticket-guia/SKILL.md +1 -1
  16. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/SKILL.md +1 -1
  17. package/src/modules/lowticket/skills/wizz-lowticket-minerador/SKILL.md +1 -1
  18. package/src/modules/lowticket/skills/wizz-lowticket-pagina/SKILL.md +1 -1
  19. package/src/modules/lowticket/skills/wizz-lowticket-trafego/SKILL.md +1 -1
  20. package/src/modules/wizz/_shared/cerebro.md +7 -7
  21. package/src/modules/wizz/_shared/communication-rules.md +1 -1
  22. package/src/modules/wizz/_shared/encerramento.md +4 -3
  23. package/src/modules/wizz/_shared/organizacao.md +8 -4
  24. package/src/modules/wizz/_shared/token-economy.md +1 -1
  25. package/src/modules/wizz/_shared/video-pipeline.md +4 -4
  26. package/src/modules/wizz/agents/wizz-ads/SKILL.md +4 -1
  27. package/src/modules/wizz/agents/wizz-ads/customize.toml +12 -3
  28. package/src/modules/wizz/agents/wizz-ads/knowledge/playbook.md +36 -0
  29. package/src/modules/wizz/agents/wizz-copy/SKILL.md +4 -1
  30. package/src/modules/wizz/agents/wizz-copy/customize.toml +2 -3
  31. package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +34 -0
  32. package/src/modules/wizz/agents/wizz-designer/SKILL.md +2 -2
  33. package/src/modules/wizz/agents/wizz-designer/customize.toml +2 -3
  34. package/src/modules/wizz/agents/wizz-growth/SKILL.md +4 -1
  35. package/src/modules/wizz/agents/wizz-growth/customize.toml +33 -4
  36. package/src/modules/wizz/agents/wizz-growth/knowledge/playbook.md +39 -0
  37. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +6 -8
  38. package/src/modules/wizz/agents/wizz-maestro/customize.toml +2 -3
  39. package/src/modules/wizz/agents/wizz-memoria/SKILL.md +7 -7
  40. package/src/modules/wizz/agents/wizz-memoria/customize.toml +5 -6
  41. package/src/modules/wizz/agents/wizz-qa/SKILL.md +5 -6
  42. package/src/modules/wizz/agents/wizz-qa/customize.toml +3 -4
  43. package/src/modules/wizz/agents/wizz-seo/SKILL.md +4 -1
  44. package/src/modules/wizz/agents/wizz-seo/customize.toml +2 -3
  45. package/src/modules/wizz/agents/wizz-seo/knowledge/playbook.md +31 -0
  46. package/src/modules/wizz/agents/wizz-social/SKILL.md +3 -3
  47. package/src/modules/wizz/agents/wizz-social/customize.toml +2 -3
  48. package/src/modules/wizz/overrides/wizz-agent-analyst.toml +2 -2
  49. package/src/modules/wizz/overrides/wizz-agent-architect.toml +3 -3
  50. package/src/modules/wizz/overrides/wizz-agent-dev.toml +3 -3
  51. package/src/modules/wizz/overrides/wizz-agent-pm.toml +2 -2
  52. package/src/modules/wizz/overrides/wizz-agent-tech-writer.toml +2 -2
  53. package/src/modules/wizz/overrides/wizz-agent-ux-designer.toml +2 -2
  54. package/src/skills-lib/_shared/complexity-signal.md +8 -9
  55. package/src/skills-lib/_shared/dispatch-rule.yaml +21 -0
  56. package/src/skills-lib/{cerebro → wizz-cerebro}/SKILL.md +1 -1
  57. package/src/skills-lib/{ctc-align → wizz-ctc-align}/SKILL.md +1 -1
  58. package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/SKILL.md +1 -1
  59. package/src/skills-lib/{find-skills → wizz-find-skills}/SKILL.md +1 -1
  60. package/src/skills-lib/wizz-huashu-design/references/sfx-library.md +1 -1
  61. package/src/skills-lib/wizz-huashu-design/scripts/add-music.sh +1 -1
  62. package/src/skills-lib/wizz-huashu-design/scripts/html2pptx.js +45 -37
  63. package/src/skills-lib/wizz-huashu-design/scripts/mix-voiceover.sh +1 -1
  64. package/src/skills-lib/wizz-huashu-design/scripts/narrate-pipeline.mjs +11 -5
  65. package/src/skills-lib/wizz-huashu-design/scripts/test_scripts.py +209 -0
  66. package/src/skills-lib/wizz-huashu-design/scripts/tts-doubao.mjs +42 -28
  67. package/src/skills-lib/wizz-ig-profile-optimizer/SKILL.md +1 -1
  68. package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +1 -1
  69. package/src/skills-lib/wizz-launch-readiness/SKILL.md +7 -7
  70. package/src/skills-lib/wizz-launch-readiness/references/02-seguranca.md +3 -3
  71. package/src/skills-lib/wizz-launch-readiness/references/03-seo-descoberta.md +5 -5
  72. package/src/skills-lib/wizz-launch-readiness/references/04-analytics-medicao.md +1 -1
  73. package/src/skills-lib/wizz-launch-readiness/references/05-conteudo-prova-social.md +6 -6
  74. package/src/skills-lib/wizz-launch-readiness/references/06-legal-lgpd.md +1 -1
  75. package/src/skills-lib/wizz-launch-readiness/references/08-fusao-priorizacao.md +1 -1
  76. package/src/skills-lib/wizz-offer-forge/SKILL.md +3 -3
  77. package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/README.md +1 -1
  78. package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/SKILL.md +1 -1
  79. package/src/skills-lib/wizz-router/SKILL.md +3 -3
  80. package/src/skills-lib/wizz-router/references/routing-table-flat.md +9 -9
  81. package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/SKILL.md +1 -1
  82. package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/SKILL.md +1 -1
  83. package/src/skills-lib/wizz-video-effects/SKILL.md +3 -3
  84. package/src/skills-lib/wizz-video-effects/references/data-contract.md +3 -3
  85. package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +15 -1
  86. package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +20 -0
  87. package/src/skills-lib/wizz-video-styles/SKILL.md +2 -2
  88. package/src/skills-lib/wizz-video-styles/data/recipes.json +32 -1
  89. package/src/skills-lib/wizz-video-styles/data/references.json +211 -1
  90. package/src/skills-lib/wizz-video-styles/data/styles.json +54 -0
  91. package/src/skills-lib/wizz-video-styles/references/generated-prompts.md +13 -3
  92. package/src/skills-lib/wizz-video-styles/references/style-picker.md +4 -2
  93. package/src/skills-lib/wizz-video-styles/references/styles/ui-morph.md +68 -0
  94. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +21 -9
  95. package/src/skills-lib/wizz-video-styles/scripts/references.mjs +1 -0
  96. package/src/skills-lib/wizz-web-security/SKILL.md +1 -1
  97. package/tools/fetch-assets.mjs +1 -1
  98. package/tools/hooks/README.md +18 -14
  99. package/tools/hooks/rtk-rewrite.js +95 -0
  100. package/tools/hooks/security-defensive-context.js +17 -1
  101. package/tools/hooks/session-rules.js +25 -5
  102. package/tools/hooks/wizz-arrumar.js +126 -12
  103. package/tools/hooks/wizz-artifact-guard.js +41 -7
  104. package/tools/hooks/wizz-cerebro-lib.js +239 -0
  105. package/tools/hooks/wizz-close-reminder.js +124 -0
  106. package/tools/hooks/wizz-debug-lib.js +55 -0
  107. package/tools/hooks/wizz-decision-trace.js +19 -2
  108. package/tools/hooks/wizz-feature.js +20 -4
  109. package/tools/hooks/wizz-router-enforce.js +30 -13
  110. package/tools/installer/command-text.js +243 -0
  111. package/tools/installer/commands/catalog.js +2 -0
  112. package/tools/installer/commands/doctor.js +10 -12
  113. package/tools/installer/commands/fetch-assets.js +5 -5
  114. package/tools/installer/commands/install.js +6 -7
  115. package/tools/installer/commands/resolve.js +17 -17
  116. package/tools/installer/commands/trace-report.js +40 -37
  117. package/tools/installer/core/doctor.js +60 -55
  118. package/tools/installer/core/hooks-settings.js +88 -14
  119. package/tools/installer/core/hooks-smoke.js +134 -0
  120. package/tools/installer/core/installer.js +61 -52
  121. package/tools/installer/core/quick-update.js +2 -3
  122. package/tools/installer/install-text.js +201 -0
  123. package/tools/installer/modules/channel-plan.js +2 -2
  124. package/tools/installer/modules/env-vars.js +5 -6
  125. package/tools/installer/modules/skills-lib.js +1 -1
  126. package/tools/installer/ui.js +17 -9
  127. /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-pesquisa-e-memoria.md +0 -0
  128. /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-rotina-e-captura.md +0 -0
  129. /package/src/skills-lib/{cerebro → wizz-cerebro}/references/comandos-sessao.md +0 -0
  130. /package/src/skills-lib/{ctc-align → wizz-ctc-align}/scripts/align.py +0 -0
  131. /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/README.md +0 -0
  132. /package/src/skills-lib/{enhance-prompt → wizz-enhance-prompt}/references/KEYWORDS.md +0 -0
  133. /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/assets/template.html +0 -0
  134. /package/src/skills-lib/{pesquisa-de-publico-do-piva → wizz-pesquisa-de-publico-do-piva}/prompts.md +0 -0
  135. /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-1-validacao-entrada.md +0 -0
  136. /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-2-autorizacao.md +0 -0
  137. /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-3-abuso-volume.md +0 -0
  138. /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-4-vazamento-resposta.md +0 -0
  139. /package/src/skills-lib/{security-audit-pentest → wizz-security-audit-pentest}/references/stage-5-fusao-plano.md +0 -0
  140. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/01-cta-principal.md +0 -0
  141. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/02-barra-fixa-mobile.md +0 -0
  142. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/03-tempo-de-resposta.md +0 -0
  143. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/04-prova-social.md +0 -0
  144. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/05-faq-decisao.md +0 -0
  145. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/06-imagens-reais.md +0 -0
  146. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/07-endereco-como-chegar.md +0 -0
  147. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/08-titles.md +0 -0
  148. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/09-open-graph.md +0 -0
  149. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/10-breadcrumbs.md +0 -0
  150. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/11-alt-text.md +0 -0
  151. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/12-schema-local.md +0 -0
  152. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/13-robots-sitemap.md +0 -0
  153. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/14-privacidade-termos.md +0 -0
  154. /package/src/skills-lib/{site-launch-kit → wizz-site-launch-kit}/references/15-medicao.md +0 -0
@@ -0,0 +1,68 @@
1
+ # UI morph: one shape, many UI states (twoclipping, verbove, AnnaCher)
2
+
3
+ Measured on 6 shorts (en) in: dev / AI / build in public (EN), product / SaaS / marketing / business / law.
4
+
5
+ ## Measured
6
+
7
+ | Metric | Value |
8
+ |---|---|
9
+ | Reach (views) | max 1,164,424 · median 2,786 |
10
+ | Cuts per 10 s | min 0.0 · median 0.0 · max 0.0 |
11
+ | Median scene length | 17.05 s |
12
+ | Music bed (continuous / faint / none) | 4 / 0 / 2 |
13
+ | Cuts with a sound effect on them | 0/0 |
14
+
15
+ Cuts come from scene detection (it undercounts overlays and cards); music and SFX from an audio heuristic that classifies type, never names the sound ([../research-method.md](../research-method.md)).
16
+
17
+ ## Material it needs
18
+
19
+ The product's real screens or a list of 8 to 12 UI states that tell its story (button, loader, player, slider, toggle, tabs, chart, ⌘K, toast), one accent colour and a track around 120 BPM. Nobody on camera, no footage: everything is drawn in code (HyperFrames).
20
+
21
+ ## When it fits
22
+
23
+ App and SaaS launches, a feature announcement, a product promo for the site or the store page, a portfolio piece. Strong on X and LinkedIn with a product audience; it shows the product working instead of describing it. Weak for teaching step by step or for anything that needs a face.
24
+
25
+ ## Recipe
26
+
27
+ **Look**
28
+
29
+ - One shape on screen the whole time, never cut: it becomes button, loader, player, slider, toggle, tabs, chart, ⌘K and toast, changing size, corners and colour (the same language as the iPhone Dynamic Island).
30
+ - Warm light grey canvas (#EDEBE6), components in black and white, at most one accent colour; a dark variant (navy with one yellow) for a product film.
31
+ - Real UI and real data: the product's own labels and numbers, never lorem ipsum or generic components.
32
+ - A cursor drives every change with real clicks and drags; the camera zooms so each state fills the frame.
33
+
34
+ **Palette:** Warm grey #EDEBE6 or beige #E6E2DA, black #111, white, plus ONE accent (orange, peach or yellow) used only where the eye must go. Dark variant: #0B1020 base, one yellow accent.
35
+
36
+ **Typography:** One UI sans (Inter, Geist or the product's own font) in regular and semibold; mono only for a URL or a code line. No display fonts.
37
+
38
+ **Motion**
39
+
40
+ - Every state grows out of the previous one by a spring (size, corner radius, colour), with at most one small overshoot; content swaps with a short blur.
41
+ - Something happens on every beat; the state changes land on the downbeats of the beat grid (scripts/beat_grid.py), not on a hand-counted 120 BPM.
42
+ - Build it as a pure function of time (seek(t)): closed-form springs, no CSS transitions or timers, so every frame renders the same. HyperFrames does this natively.
43
+ - The last frame equals the first, so the piece loops.
44
+
45
+ **Captions:** None. The text is the interface's own (button labels, track title, chart numbers). A single short line at most, in the UI font, when the product needs one sentence.
46
+
47
+ **Hook (0 to 3 s):** The first state already on screen at 0 s (a button or the brand pill) and the cursor arriving on the first beat. No logo intro, no title card.
48
+
49
+ **CTA:** The shape returns to the first state or closes on the brand pill with the URL in small mono under it, held 1 to 2 s.
50
+
51
+ **Music:** The track is the whole soundtrack: a continuous bed in 4 of 6 measured pieces, around -14 LUFS. Pick an electronic or lo-fi track with a clear kick so the beat grid is reliable.
52
+
53
+ **SFX:** Sparse and tied to UI events: a soft click on a press, a low tick on a snap. The measured pieces range from none to a hit on most state changes; never whooshes on every move.
54
+
55
+ ## Do not
56
+
57
+ - Cutting between screens: the moment it cuts it becomes a screen recording, not this style.
58
+ - Bouncy easing, particles, glow, gradients on the UI chrome, icons with uneven stroke, dead time, template look.
59
+ - Generic components instead of the product's real UI and numbers.
60
+ - Shipping square or 16:9: every measured piece is 1:1 or 16:9; recompose for 9:16 with the shape in the upper-middle and nothing in the bottom 20 %.
61
+
62
+ ## Watch before editing
63
+
64
+ - [zero](https://x.com/twoclipping/status/2103273003555402193) · 1,164,424 views
65
+ - [Martijn Verbove](https://x.com/verbove/status/2103483957266268381) · 17,438 views
66
+ - [Daniel Haida](https://x.com/daniel_haida/status/2104139720829636937) · 3,968 views
67
+ - [Anna Chernikova](https://x.com/AnnaCher___/status/2103571096549433425) · 1,605 views
68
+ - [Nez](https://x.com/nezbuilds/status/2103487427365294391) · 519 views
@@ -1,12 +1,25 @@
1
1
  #!/usr/bin/env bash
2
- # Measures one public YouTube short: download (720p), cuts (scene detection),
2
+ # Measures one public short: download (720p), cuts (scene detection),
3
3
  # audio (music bed, SFX type heuristic, loudness) and a 16-frame contact sheet.
4
- # Usage: analyze.sh <youtube_id> <out_dir> → <out_dir>/<id>.json + <id>-sheet.png
4
+ # Usage: analyze.sh <youtube_id|url> <out_dir> → <out_dir>/<id>.json + <id>-sheet.png
5
+ # A URL (an X post, any page yt-dlp reads) is measured the same way; its id is
6
+ # x-<author>-<status> for X and a sanitised slug otherwise (2026-10-15: the
7
+ # code-generated UI pieces of the ui-morph style live on X, not on YouTube).
5
8
  # The .mp4 is deleted at the end; read the contact sheet before describing the look.
6
9
  set -euo pipefail
7
- [ $# -eq 2 ] || { echo "uso: analyze.sh <youtube_id> <out_dir>" >&2; exit 2; }
10
+ [ $# -eq 2 ] || { echo "uso: analyze.sh <youtube_id|url> <out_dir>" >&2; exit 2; }
8
11
  id="$1"; out="$2"
9
- [[ "$id" =~ ^[A-Za-z0-9_-]{11}$ ]] || { echo "id inválido: $id" >&2; exit 2; }
12
+ if [[ "$id" =~ ^https?:// ]]; then
13
+ url="$id"
14
+ if [[ "$url" =~ (x|twitter)\.com/([A-Za-z0-9_]+)/status/([0-9]+) ]]; then
15
+ id="x-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}"
16
+ else
17
+ id=$(printf '%s' "$url" | sed -E 's#^https?://##; s#[^A-Za-z0-9_-]+#-#g' | cut -c1-60)
18
+ fi
19
+ else
20
+ [[ "$id" =~ ^[A-Za-z0-9_-]{11}$ ]] || { echo "id inválido: $id" >&2; exit 2; }
21
+ url="https://www.youtube.com/shorts/$id"
22
+ fi
10
23
  mkdir -p "$out"; cd "$out"
11
24
  f="./$id" # prefixo ./ : ID que começa com hífen não vira opção do ffmpeg
12
25
  # .part: a first download that fails (HTTP 403) leaves it behind before the android fallback runs.
@@ -15,12 +28,11 @@ trap 'rm -f "$f.wav" "$f.mp4" "$f".*.part' EXIT
15
28
  PY="${VIDEO_STYLES_PY:-python3}"
16
29
  "$PY" -c "import librosa, numpy" 2>/dev/null || { echo "precisa de librosa e numpy: pip install librosa numpy (ou VIDEO_STYLES_PY=/caminho/do/python)" >&2; exit 2; }
17
30
  for bin in yt-dlp ffmpeg ffprobe; do command -v "$bin" >/dev/null || { echo "falta $bin no PATH" >&2; exit 2; }; done
18
- url="https://www.youtube.com/shorts/$id"
19
31
  fmt="bv*[height<=1280][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b"
20
32
  # When YouTube answers "confirm you're not a bot", the android player client usually still works (no cookies used).
21
33
  [ -f "$f.mp4" ] || yt-dlp -q --no-warnings -f "$fmt" --merge-output-format mp4 -o "$f.mp4" "$url" \
22
34
  || yt-dlp -q --no-warnings --extractor-args "youtube:player_client=android" -f "$fmt" --merge-output-format mp4 -o "$f.mp4" "$url"
23
- meta_fmt="%(view_count)s|%(like_count)s|%(channel)s|%(upload_date)s|%(title)s"
35
+ meta_fmt="%(view_count)s|%(like_count)s|%(channel,uploader)s|%(upload_date)s|%(title)s"
24
36
  yt-dlp -q --no-warnings --skip-download --print "$meta_fmt" "$url" > "$f.meta" \
25
37
  || yt-dlp -q --no-warnings --skip-download --extractor-args "youtube:player_client=android" --print "$meta_fmt" "$url" > "$f.meta" \
26
38
  || echo "NA|NA|NA|NA|NA" > "$f.meta"
@@ -32,9 +44,9 @@ lufs=$(ffmpeg -i "$f.mp4" -af ebur128 -f null - 2>&1 | grep -E "^\s+I:" | tail -
32
44
  d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f.mp4")
33
45
  step=$(python3 -c "print(max(1.0, $d/16))")
34
46
  ffmpeg -loglevel error -y -i "$f.mp4" -vf "fps=1/$step,scale=200:-1,tile=8x2" -frames:v 1 "$f-sheet.png"
35
- "$PY" - "$id" "$lufs" <<'PY'
47
+ "$PY" - "$id" "$lufs" "$url" <<'PY'
36
48
  import librosa, numpy as np, json, sys
37
- id, lufs = sys.argv[1], sys.argv[2]
49
+ id, lufs, url = sys.argv[1], sys.argv[2], sys.argv[3]
38
50
  y, sr = librosa.load(f"{id}.wav", sr=22050); dur = len(y)/sr
39
51
  cuts = [float(x) for x in open(f"{id}.cuts").read().split()]
40
52
  H, P = librosa.effects.hpss(y, margin=2.0)
@@ -58,7 +70,7 @@ on_cut = sum(1 for c in cuts if any(abs(t-c) < 0.15 for t,_ in events))
58
70
  shots = np.diff([0]+cuts+[dur]) if dur else np.array([0])
59
71
  bed = float(np.percentile(rh,15)/(np.percentile(rh,95)+1e-9))
60
72
  meta = open(f"{id}.meta").read().strip().split("|", 4)
61
- json.dump({"id": id, "url": f"https://www.youtube.com/shorts/{id}", "views": meta[0], "likes": meta[1], "channel": meta[2], "upload_date": meta[3], "title": meta[4],
73
+ json.dump({"id": id, "url": url, "views": meta[0], "likes": meta[1], "channel": meta[2], "upload_date": meta[3], "title": meta[4],
62
74
  "duration_s": round(dur,1), "cuts": len(cuts), "cuts_per_10s": round(len(cuts)/dur*10,2), "shot_median_s": round(float(np.median(shots)),2), "shot_mean_s": round(float(shots.mean()),2),
63
75
  "music_bed_ratio": round(bed,3), "music_bed_guess": "continua" if bed > 0.13 else ("fraca" if bed > 0.07 else "sem"),
64
76
  "sfx_counts": kinds, "sfx_per_10s": round(sum(kinds.values())/dur*10,2), "sfx_on_cuts": f"{on_cut}/{len(cuts)}", "sfx_events": events[:40],
@@ -31,6 +31,7 @@ const HISTORY_REL = path.join('_wizz-output', 'video-styles', 'historico.json');
31
31
  // tour, macro...) have no entry: a code-generated video is not a reference for them.
32
32
  const GENERATED = {
33
33
  'tipografia-cinetica': { categories: ['motion', 'explainer', '3d'], match: /lyric|music video|kinetic typograph|\bMV\b/i },
34
+ 'ui-morph': { categories: ['motion', 'explainer', '3d', 'interactive'], match: /UI states|one shape|morph|UI moments|product film|inside the app/i },
34
35
  'motion-editorial': { categories: ['motion', 'explainer'], match: /\bUI\b|product|launch|saas|\bapp\b|logo|promo|brand/i },
35
36
  'explicador-animado': { categories: ['explainer'], match: /./ },
36
37
  'quadro-branco': { categories: ['explainer', 'motion'], match: /whiteboard|hand-drawn|sketch|doodle/i },
@@ -32,7 +32,7 @@ The ones that show up most in an audit, with the direct fix. Use as a quick tria
32
32
 
33
33
  > The first four (SQLi, IDOR, mass assignment, and permissive RLS) are **stop-the-line**: if it shows up, fix it before anything else.
34
34
  >
35
- > For a full adversarial audit (hunting with exploit proof + a prioritized plan), use the `security-audit-pentest` skill. This skill here is for **fixing** a specific finding from the table.
35
+ > For a full adversarial audit (hunting with exploit proof + a prioritized plan), use the `wizz-security-audit-pentest` skill. This skill here is for **fixing** a specific finding from the table.
36
36
 
37
37
  ## References (load on demand)
38
38
 
@@ -39,7 +39,7 @@ const __filename = fileURLToPath(import.meta.url);
39
39
  const __dirname = path.dirname(__filename);
40
40
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
41
41
 
42
- export const ASSETS_BASE = 'https://wizz-assets.junior-aec.workers.dev';
42
+ export const ASSETS_BASE = 'https://assets.wizzcomms.com';
43
43
 
44
44
  // Fonte única dos bundles conhecidos. `dest` é relativo ao root do pacote
45
45
  // (repo em dev, node_modules/wizz-method quando instalado via npm — a mesma
@@ -4,29 +4,33 @@ Os hooks do Claude Code usados pelo Wizz Method vivem AQUI e são instalados em
4
4
  `~/.claude/hooks/` via `npm run sync:global`. Nunca edite a cópia global direto:
5
5
  ela é substituída no próximo sync.
6
6
 
7
- | Hook | Evento | O que faz |
8
- | ------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | `wizz-router-enforce.js` | UserPromptSubmit | Gate `isTrivial()` + injeção curta: em projeto Wizz (`_wizz/`) manda delegar direto ao maestro/agente da área (sem router); fora, manda pro `wizz-router`. |
10
- | `session-rules.js` | SessionStart | Duas coisas num processo só: injeta as regras de comunicação 1x por sessão (antes era por prompt) e resolve a FEATURE ATIVA sozinho (branch git > `_wizz/bmm/config.yaml` > nada), para ninguém precisar invocar `wizz-set-feature`. Branch de tronco e de release não viram feature. Fora de projeto Wizz e sem branch de feature a linha não é emitida (custo zero); dentro de projeto Wizz sem feature sai o lembrete curto "o primeiro pedido nomeado define uma", e sem `layout:` o lembrete de propor um. A feature também é o `tema` da memória. |
11
- | `security-defensive-context.js` | PreToolUse (Skill) | Metodologia defensiva/escopada ao invocar skills de segurança. Genérico por projeto (v2). |
12
- | `wizz-artifact-guard.js` | PostToolUse (Write) | 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
- | `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). |
7
+ | Hook | Evento | O que faz |
8
+ | ------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `wizz-router-enforce.js` | UserPromptSubmit | Gate `isTrivial()` + injeção curta: em projeto Wizz (`_wizz/`) manda delegar direto ao maestro/agente da área (sem router); fora, manda pro `wizz-router`. |
10
+ | `session-rules.js` | SessionStart | Duas coisas num processo só: injeta as regras de comunicação 1x por sessão (antes era por prompt) e resolve a FEATURE ATIVA sozinho (branch git > `_wizz/bmm/config.yaml` > nada), para ninguém precisar invocar `wizz-set-feature`. Branch de tronco e de release não viram feature. Fora de projeto Wizz e sem branch de feature a linha não é emitida (custo zero); dentro de projeto Wizz sem feature sai o lembrete curto "o primeiro pedido nomeado define uma", e sem `layout:` o lembrete de propor um. A feature também é o `tema` da memória. |
11
+ | `security-defensive-context.js` | PreToolUse (Skill) | Metodologia defensiva/escopada ao invocar skills de segurança. Genérico por projeto (v2). |
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
+ | `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
+ | `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. |
14
16
 
15
17
  Arquivos de apoio (copiados junto, NUNCA registrados em settings.json; o doctor cobra a presença):
16
18
 
17
- | Arquivo | O que é |
18
- | --- | --- |
19
- | `wizz-feature-lib.js` | Funções puras de feature, git, config, pastas de artefato e frontmatter. Usada por todos os hooks acima. |
20
- | `wizz-layout-lib.js` | O que pode ficar na raiz e para onde vai o resto (tipos `codigo`, `scripts`, `dados`, `docs`, `midia`, `arquivo`). |
21
- | `wizz-feature.js` | CLI do ciclo de vida da feature: `resolver`, `definir`, `artefato`, `decidir`, `status`, `verificar`, `encerrar`, `listar`. É o passo verificável que router/maestro rodam antes de despachar. |
22
- | `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-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`. |
23
27
 
24
28
  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.
25
29
 
26
30
  Removidos em 2026-07 (arquivados em `~/.claude/hooks-archive/`):
27
31
  `no-narration-enforce.js` (absorvido pelo session-rules) e `vault-reminder.js`.
28
32
 
29
- Testes: `npm run test:organizacao` (CLIs de feature e arrumação, guard, lembretes nos cenários sem git / main / branch), `npm run eval:feature` (hooks reais + CLI + estado final; `--llm` mede a obediência do modelo), `npm run test:hooks` (isTrivial + contexto por modo), `npm run test:feature-context` (resolução da feature ativa: tronco não vira feature, branch ganha do config, silêncio quando não há feature) e `node test/test-rtk-hook.js` (rewrite básico + multi-linha do `rtk-rewrite.sh`, smoke test que exercita o binário `rtk` real quando disponível, e o cache do version-guard: criado na 1ª chamada e reusado na 2ª, TTL expirado força recompute, path do binário mudou invalida, cache corrompido não quebra e se auto-cura).
33
+ Testes: `node test/test-feature-close.js` (`encerrar --cerebro` e o lembrete do Stop), `npm run test:organizacao` (CLIs de feature e arrumação, guard, lembretes nos cenários sem git / main / branch), `npm run eval:feature` (hooks reais + CLI + estado final; `--llm` mede a obediência do modelo), `npm run test:hooks` (isTrivial + contexto por modo), `npm run test:feature-context` (resolução da feature ativa: tronco não vira feature, branch ganha do config, silêncio quando não há feature) e `node test/test-rtk-hook.js` (rewrite básico + multi-linha do `rtk-rewrite.sh`, smoke test que exercita o binário `rtk` real quando disponível, e o cache do version-guard: criado na 1ª chamada e reusado na 2ª, TTL expirado força recompute, path do binário mudou invalida, cache corrompido não quebra e se auto-cura).
30
34
  Evals de roteamento: `node evals/routing/run-routing-eval.mjs [--llm]`.
31
35
 
32
36
  Drift repo ↔ global: `npm run sync:check` compara hash de cada hook (e skill) entre o repo e `~/.claude`, sem escrever nada — é lembrete (fail-open), não gate; roda no fim do `npm test`. Achou drift? `npm run sync:global` atualiza `~/.claude`.
@@ -0,0 +1,95 @@
1
+ // rtk-rewrite.js — PreToolUse (Bash): reescreve o comando via `rtk rewrite`.
2
+ //
3
+ // Versão Node do rtk-rewrite.sh para o Windows (2026-10-15), onde não há bash
4
+ // nem jq garantidos. O installer registra este arquivo em win32 e o .sh nos
5
+ // outros sistemas (hooks-settings.js), então o comportamento tem de ser o
6
+ // mesmo; test/test-rtk-hook-node.js roda os dois contra o mesmo rtk fake.
7
+ //
8
+ // Mesmo contrato do .sh:
9
+ // - sem rtk no PATH, ou rtk abaixo de 0.23.0: sai 0 sem tocar o comando;
10
+ // - comando multi-linha com heredoc, continuação `\` ou aspas abertas numa
11
+ // linha passa intacto (reescrever corromperia o corpo do heredoc);
12
+ // - cada linha vai sozinha para `rtk rewrite`, com timeout (RTK_HOOK_TIMEOUT,
13
+ // 3 s); exit 0 ou 3 com saída = linha nova, qualquer outra coisa = original;
14
+ // - nada mudou: stdout vazio. Mudou: hookSpecificOutput com updatedInput.
15
+ // Fail-open em tudo: erro aqui nunca bloqueia o Bash.
16
+ //
17
+ // Sem o cache de versão do .sh: `rtk --version` de um binário nativo custa
18
+ // poucos ms, e no Windows não há `command -v` para chavear o cache pelo path.
19
+
20
+ const { spawnSync } = require('node:child_process');
21
+
22
+ const TIMEOUT_MS = (Number(process.env.RTK_HOOK_TIMEOUT) || 3) * 1000;
23
+ const FLOOR = [0, 23];
24
+
25
+ function rtkVersionOk() {
26
+ const res = spawnSync('rtk', ['--version'], { encoding: 'utf8', timeout: TIMEOUT_MS, windowsHide: true });
27
+ if (res.error || res.status !== 0) {
28
+ process.stderr.write('[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands.\n');
29
+ return false;
30
+ }
31
+ const match = /v?(\d+)\.(\d+)\.(\d+)/.exec(res.stdout || '');
32
+ if (!match) return true;
33
+ const [major, minor] = [Number(match[1]), Number(match[2])];
34
+ if (major === FLOOR[0] && minor < FLOOR[1]) {
35
+ process.stderr.write(
36
+ `[rtk] WARNING: rtk ${match[1]}.${match[2]}.${match[3]} is too old (need >= 0.23.0). Upgrade: cargo install rtk\n`,
37
+ );
38
+ return false;
39
+ }
40
+ return true;
41
+ }
42
+
43
+ // Heredoc, continuação ou aspas desbalanceadas numa linha: não dá para
44
+ // reescrever linha a linha sem quebrar o comando.
45
+ function multilineUnsafe(cmd) {
46
+ if (cmd.includes('<<')) return true;
47
+ return cmd.split('\n').some((line) => {
48
+ if (line.endsWith('\\')) return true;
49
+ const count = (ch) => line.split(ch).length - 1;
50
+ return count('"') % 2 !== 0 || count("'") % 2 !== 0;
51
+ });
52
+ }
53
+
54
+ function rewriteOne(line) {
55
+ if (!line) return line;
56
+ const res = spawnSync('rtk', ['rewrite', line], { encoding: 'utf8', timeout: TIMEOUT_MS, windowsHide: true });
57
+ // Como o $(...) do .sh: a saída perde só as quebras de linha do fim.
58
+ const out = (res.stdout || '').replace(/\n+$/, '');
59
+ return !res.error && (res.status === 0 || res.status === 3) && out ? out : line;
60
+ }
61
+
62
+ function main(raw) {
63
+ const input = JSON.parse(raw);
64
+ const toolInput = input && typeof input.tool_input === 'object' && input.tool_input ? input.tool_input : {};
65
+ const cmd = typeof toolInput.command === 'string' ? toolInput.command : '';
66
+ if (!cmd) return;
67
+ if (cmd.includes('\n') && multilineUnsafe(cmd)) return;
68
+ if (!rtkVersionOk()) return;
69
+
70
+ const rewritten = cmd.includes('\n') ? cmd.split('\n').map(rewriteOne).join('\n') : rewriteOne(cmd);
71
+ if (rewritten === cmd) return;
72
+
73
+ process.stdout.write(
74
+ JSON.stringify({
75
+ hookSpecificOutput: {
76
+ hookEventName: 'PreToolUse',
77
+ permissionDecision: 'allow',
78
+ permissionDecisionReason: 'RTK auto-rewrite',
79
+ updatedInput: { ...toolInput, command: rewritten },
80
+ },
81
+ }) + '\n',
82
+ );
83
+ }
84
+
85
+ let raw = '';
86
+ process.stdin.setEncoding('utf8');
87
+ process.stdin.on('data', (chunk) => (raw += chunk));
88
+ process.stdin.on('end', () => {
89
+ try {
90
+ main(raw);
91
+ } catch {
92
+ // fail-open: JSON quebrado ou erro inesperado deixa o comando como veio
93
+ }
94
+ process.exit(0);
95
+ });
@@ -28,6 +28,21 @@ const SECURITY_SKILLS = new Set([
28
28
  'code-review:code-review',
29
29
  ]);
30
30
 
31
+ // Diagnóstico opt-in (WIZZ_DEBUG=1). Lib ausente numa cópia parcial: ainda avisa em stderr.
32
+ let debug = (hook, phase, err) => {
33
+ if (process.env.WIZZ_DEBUG !== '1') return;
34
+ try {
35
+ process.stderr.write(`[wizz-debug] ${hook} ${phase}: ${(err && err.message) || err}\n`);
36
+ } catch {
37
+ // stderr fechado
38
+ }
39
+ };
40
+ try {
41
+ ({ debug } = require('./wizz-debug-lib.js'));
42
+ } catch (error) {
43
+ debug('security-defensive-context', 'require wizz-debug-lib', error);
44
+ }
45
+
31
46
  const DEFENSIVE_CONTEXT =
32
47
  'Metodologia de revisao de seguranca (defensiva, escopada):\n\n' +
33
48
  '1. ESCOPO: trabalhe por arquivo ou dimensao concreta de cada vez (ex: validacao ' +
@@ -63,8 +78,9 @@ process.stdin.on('end', () => {
63
78
  );
64
79
  }
65
80
  // Para qualquer outra skill/tool: sai silenciosamente sem output
66
- } catch {
81
+ } catch (error) {
67
82
  // Parse error: sai silenciosamente, nao bloqueia nada
83
+ debug('security-defensive-context', 'parse payload', error);
68
84
  }
69
85
  process.exit(0);
70
86
  });
@@ -45,11 +45,27 @@ const RULES =
45
45
  // Feature, git e config moram em wizz-feature-lib.js (mesma pasta: o installer
46
46
  // e o sync:global copiam tools/hooks/ inteiro). Se a lib faltar numa cópia
47
47
  // manual parcial, a regra de comunicação continua saindo e a feature cala.
48
+ // Diagnóstico opt-in (WIZZ_DEBUG=1). Lib ausente numa cópia parcial: ainda avisa em stderr.
49
+ let debug = (hook, phase, err) => {
50
+ if (process.env.WIZZ_DEBUG !== '1') return;
51
+ try {
52
+ process.stderr.write(`[wizz-debug] ${hook} ${phase}: ${(err && err.message) || err}\n`);
53
+ } catch {
54
+ // stderr fechado
55
+ }
56
+ };
57
+ try {
58
+ ({ debug } = require('./wizz-debug-lib.js'));
59
+ } catch (error) {
60
+ debug('session-rules', 'require wizz-debug-lib', error);
61
+ }
62
+
48
63
  let lib = null;
49
64
  try {
50
65
  lib = require('./wizz-feature-lib.js');
51
- } catch {
66
+ } catch (error) {
52
67
  lib = null;
68
+ debug('session-rules', 'require wizz-feature-lib', error);
53
69
  }
54
70
 
55
71
  const HOOK_CMD = 'node _wizz/hooks/wizz-feature.js';
@@ -246,22 +262,25 @@ function finish(raw) {
246
262
  try {
247
263
  const payload = JSON.parse(raw || '{}');
248
264
  if (payload && typeof payload.cwd === 'string' && payload.cwd) cwd = payload.cwd;
249
- } catch {
265
+ } catch (error) {
250
266
  // payload ausente ou inválido: cai no cwd do processo
267
+ debug('session-rules', 'parse payload', error);
251
268
  }
252
269
 
253
270
  try {
254
271
  const feature = buildFeatureContext(cwd);
255
272
  if (feature) context += `\n\n${feature}`;
256
- } catch {
273
+ } catch (error) {
257
274
  // resolução de feature NUNCA derruba as regras de comunicação
275
+ debug('session-rules', 'buildFeatureContext', error);
258
276
  }
259
277
 
260
278
  try {
261
279
  const ceiling = buildMemoryCeilingContext(cwd);
262
280
  if (ceiling) context += `\n\n${ceiling}`;
263
- } catch {
281
+ } catch (error) {
264
282
  // teto da memória é aviso, não gate: erro aqui não afeta o resto
283
+ debug('session-rules', 'buildMemoryCeilingContext', error);
265
284
  }
266
285
 
267
286
  try {
@@ -273,8 +292,9 @@ function finish(raw) {
273
292
  },
274
293
  }),
275
294
  );
276
- } catch {
295
+ } catch (error) {
277
296
  // nunca bloquear
297
+ debug('session-rules', 'write stdout', error);
278
298
  }
279
299
  process.exit(0);
280
300
  }
@@ -14,8 +14,10 @@
14
14
  // mover <origem> <destino> atalho de um arquivo (manifesto de uma linha + aplicar)
15
15
  // desfazer <pasta-da-arrumacao>
16
16
  //
17
- // Opções de aplicar/mover: --testes "<comando>", --refs <dir,dir> (ex.: pasta
18
- // da auto-memória), --manter (não desfaz sozinho se os testes quebrarem).
17
+ // Opções de aplicar/mover: --testes "<comando>" (só runner conhecido: npm/pnpm/
18
+ // yarn/bun test, npx vitest|jest, pytest, go/cargo/make test; sem valor = script
19
+ // test do package.json; outro comando exige --testes-livre), --refs <dir,dir>
20
+ // (ex.: pasta da auto-memória), --manter (não desfaz sozinho se os testes quebrarem).
19
21
  // Cada execução fica em `<arquivo>/arrumacao/<data>-<hora>/`.
20
22
 
21
23
  const fs = require('node:fs');
@@ -76,7 +78,7 @@ function parseArgs(argv) {
76
78
  if (a.startsWith('--')) {
77
79
  const key = a.slice(2);
78
80
  const next = argv[i + 1];
79
- if (next !== undefined && !next.startsWith('--')) {
81
+ if (key !== 'testes-livre' && next !== undefined && !next.startsWith('--')) {
80
82
  opts[key] = next;
81
83
  i++;
82
84
  } else opts[key] = true;
@@ -237,17 +239,116 @@ function rewriteRefs(text, moves, fileDir) {
237
239
 
238
240
  // ── aplicar / desfazer ──────────────────────────────────────────────────
239
241
 
240
- function runTests(root, cmd) {
241
- if (!cmd || cmd === true) return null;
242
- const r = spawnSync(cmd, { cwd: root, shell: true, encoding: 'utf8', timeout: 10 * 60 * 1000 });
243
- return { ok: r.status === 0, code: r.status, tail: `${r.stdout || ''}${r.stderr || ''}`.trim().split('\n').slice(-5).join('\n') };
242
+ // ── comando de testes: allowlist, sem shell ─────────────────────────────
243
+ //
244
+ // `--testes` não executa string arbitrária: o comando é cortado em argv (sem
245
+ // shell) e só roda se for de um runner conhecido. `--testes` sem valor usa o
246
+ // script `test` do package.json. Qualquer outra coisa exige `--testes-livre`,
247
+ // que imprime o que vai rodar e usa o shell (o dono assumiu o comando).
248
+
249
+ const SHELL_META = /[;&|<>$`()\n\r\\]/;
250
+ const PM = new Set(['npm', 'pnpm', 'yarn', 'bun']);
251
+ const TEST_SCRIPT = /^test(?:[:\w.-]*)$/;
252
+
253
+ /** Corta em argv respeitando aspas simples/duplas. null = não dá para cortar com segurança. */
254
+ function splitArgv(text) {
255
+ if (SHELL_META.test(text)) return null;
256
+ const argv = [];
257
+ let cur = '';
258
+ let quote = '';
259
+ let has = false;
260
+ for (const ch of text) {
261
+ if (quote) {
262
+ if (ch === quote) quote = '';
263
+ else cur += ch;
264
+ } else if (ch === '"' || ch === "'") {
265
+ quote = ch;
266
+ has = true;
267
+ } else if (/\s/.test(ch)) {
268
+ if (has || cur) argv.push(cur);
269
+ cur = '';
270
+ has = false;
271
+ } else {
272
+ cur += ch;
273
+ has = true;
274
+ }
275
+ }
276
+ if (quote) return null;
277
+ if (has || cur) argv.push(cur);
278
+ return argv;
279
+ }
280
+
281
+ /** O argv é de um runner de testes conhecido? */
282
+ function isAllowedTestArgv(argv) {
283
+ const [bin, a, b] = argv;
284
+ if (!bin) return false;
285
+ if (PM.has(bin)) return a === 'test' || (a === 'run' && TEST_SCRIPT.test(b || ''));
286
+ if (bin === 'npx') return a === 'vitest' || a === 'jest';
287
+ if (bin === 'pytest') return true;
288
+ if (bin === 'python' || bin === 'python3') return a === '-m' && (b === 'pytest' || b === 'unittest');
289
+ if (bin === 'go' || bin === 'cargo' || bin === 'make') return a === 'test';
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Resolve o que `--testes` vai rodar.
295
+ * @returns {{none: true}|{argv: string[], shell: false, display: string}|{shellCmd: string, shell: true, display: string}|{error: string}}
296
+ */
297
+ function resolveTestCommand(root, opts) {
298
+ const raw = opts.testes;
299
+ if (!raw) return { none: true };
300
+ let text = raw;
301
+ if (raw === true) {
302
+ try {
303
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
304
+ if (!pkg.scripts || typeof pkg.scripts.test !== 'string') throw new Error('sem script test');
305
+ } catch {
306
+ return {
307
+ error: '--testes sem comando pede o script "test" do package.json, e ele não existe aqui. Passe o comando ou use --testes-livre.',
308
+ };
309
+ }
310
+ text = 'npm test';
311
+ }
312
+ const argv = splitArgv(text);
313
+ if (argv && isAllowedTestArgv(argv)) return { argv, shell: false, display: argv.join(' ') };
314
+ if (opts['testes-livre']) return { shellCmd: text, shell: true, display: text };
315
+ return {
316
+ error:
317
+ `Comando de testes fora da lista: "${text}". Aceitos: npm/pnpm/yarn/bun test ou run test*, npx vitest|jest, pytest, ` +
318
+ 'python -m pytest|unittest, go test, cargo test, make test (sem ; & | < > $ ` ou parênteses). ' +
319
+ 'Para rodar outro comando, repita com --testes-livre.',
320
+ };
321
+ }
322
+
323
+ function runTests(root, resolved) {
324
+ if (!resolved || resolved.none) return null;
325
+ process.stderr.write(`Rodando testes${resolved.shell ? ' (livre, via shell)' : ''}: ${resolved.display}\n`);
326
+ const timeout = 10 * 60 * 1000;
327
+ const r = resolved.shell
328
+ ? spawnSync(resolved.shellCmd, { cwd: root, shell: true, encoding: 'utf8', timeout })
329
+ : spawnSync(resolved.argv[0], resolved.argv.slice(1), { cwd: root, shell: false, encoding: 'utf8', timeout });
330
+ const out = `${r.stdout || ''}${r.stderr || ''}${r.error ? r.error.message : ''}`;
331
+ return { ok: r.status === 0, code: r.status, tail: out.trim().split('\n').slice(-5).join('\n') };
244
332
  }
245
333
 
246
334
  function shQuote(s) {
247
335
  return `'${String(s).replaceAll("'", String.raw`'\''`)}'`;
248
336
  }
249
337
 
250
- function writeUndoScript(runDir, root, moves, edits) {
338
+ // Desfazer em qualquer sistema: `node wizz-arrumar.js desfazer <pasta>` (Node
339
+ // puro, função undo abaixo). O script ao lado é atalho: desfazer.sh fora do
340
+ // Windows; no Windows, sem bash garantido, um desfazer.cmd que chama o Node.
341
+ function undoCommand(root, runDir) {
342
+ const q = (p) => `"${p}"`;
343
+ return `node ${q(__filename)} desfazer ${q(path.relative(root, runDir))}`;
344
+ }
345
+
346
+ function writeUndoScript(runDir, root, moves, edits, platform = process.platform) {
347
+ if (platform === 'win32') {
348
+ const file = path.join(runDir, 'desfazer.cmd');
349
+ fs.writeFileSync(file, ['@echo off', `cd /d "${root}"`, undoCommand(root, runDir), ''].join('\r\n'));
350
+ return file;
351
+ }
251
352
  const lines = [
252
353
  '#!/usr/bin/env bash',
253
354
  `# Desfaz a arrumação de ${path.basename(runDir)}: restaura os arquivos editados e devolve os moves.`,
@@ -305,12 +406,14 @@ function applyMoves(root, moves, opts) {
305
406
  const config = lib.readConfig(root);
306
407
  const problems = validateMoves(root, moves);
307
408
  if (problems.length > 0) return { ok: false, code: 2, line: `Nada foi movido: ${problems.join('; ')}.` };
409
+ const testCommand = resolveTestCommand(root, opts);
410
+ if (testCommand.error) return { ok: false, code: 2, line: `Nada foi movido: ${testCommand.error}` };
308
411
 
309
412
  const runDir = path.join(archiveDir(root, config.layout), stamp());
310
413
  fs.mkdirSync(runDir, { recursive: true });
311
414
  writeManifest(path.join(runDir, 'manifesto.tsv'), moves);
312
415
 
313
- const before = runTests(root, opts.testes);
416
+ const before = runTests(root, testCommand);
314
417
 
315
418
  // Referências: projeto inteiro + pastas extras (auto-memória, Cérebro).
316
419
  const extra = typeof opts.refs === 'string' ? opts.refs.split(',').map((d) => path.resolve(root, d.trim())) : [];
@@ -348,7 +451,7 @@ function applyMoves(root, moves, opts) {
348
451
  fs.renameSync(path.join(root, m.origem), to);
349
452
  }
350
453
  const undoFile = writeUndoScript(runDir, root, moves, edits);
351
- const after = runTests(root, opts.testes);
454
+ const after = runTests(root, testCommand);
352
455
  const relRun = path.relative(root, runDir);
353
456
 
354
457
  if (before && after && before.ok && !after.ok && !opts.manter) {
@@ -365,7 +468,7 @@ function applyMoves(root, moves, opts) {
365
468
  return {
366
469
  ok: !after || after.ok || !before?.ok,
367
470
  code: 0,
368
- line: `Movi ${moves.length} arquivo(s) e atualizei referências em ${edits.length}.${testes} Desfazer: ${path.relative(root, undoFile)}.`,
471
+ line: `Movi ${moves.length} arquivo(s) e atualizei referências em ${edits.length}.${testes} Desfazer: ${undoCommand(root, runDir)} (ou ${path.relative(root, undoFile)}).`,
369
472
  };
370
473
  }
371
474
 
@@ -415,4 +518,15 @@ if (require.main === module) {
415
518
  process.exit(result.code ?? (result.ok ? 0 : 1));
416
519
  }
417
520
 
418
- module.exports = { run, rewriteRefs, readManifest, planMoves, undo };
521
+ module.exports = {
522
+ run,
523
+ splitArgv,
524
+ isAllowedTestArgv,
525
+ resolveTestCommand,
526
+ rewriteRefs,
527
+ readManifest,
528
+ planMoves,
529
+ undo,
530
+ writeUndoScript,
531
+ undoCommand,
532
+ };