wizz-method 2.21.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +18 -0
- package/package.json +4 -2
- package/skills-registry.yaml +41 -0
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +8 -1
- package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +2 -2
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-03-implement.md +2 -0
- package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +1 -1
- package/src/core-skills/wizz-party-mode/customize.toml +3 -3
- package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +2 -0
- package/src/modules/wizz/_shared/cerebro.md +1 -1
- package/src/modules/wizz/_shared/organizacao.md +2 -2
- package/src/modules/wizz/agents/wizz-copy/SKILL.md +1 -0
- package/src/modules/wizz/agents/wizz-copy/customize.toml +5 -0
- package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +2 -1
- package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +14 -0
- package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +12 -0
- package/src/skills-lib/wizz-cerebro/SKILL.md +18 -5
- package/src/skills-lib/wizz-design-needs/SKILL.md +7 -0
- package/src/skills-lib/wizz-design-needs/references/design-state.md +34 -2
- package/src/skills-lib/wizz-design-needs/references/gap-analysis.md +1 -1
- package/src/skills-lib/wizz-design-needs/scripts/design_state.py +305 -0
- package/src/skills-lib/wizz-design-needs/scripts/tests/test_design_state.py +143 -0
- package/src/skills-lib/wizz-impeccable/references/design-rules.md +18 -0
- package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +2 -0
- package/src/skills-lib/wizz-paid-ads/SKILL.md +2 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +6 -2
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +7 -34
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +2 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/design-sources.yaml +252 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +1 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mining-rubric.md +123 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +10 -4
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/web-access-and-evidence.md +1 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/design_sources.py +221 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/tests/test_design_sources.py +130 -0
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
- package/src/skills-lib/wizz-taste-skill/references/anti-slop-tells.md +8 -0
- package/src/skills-lib/wizz-ui-component-curator/SKILL.md +8 -7
- package/src/skills-lib/wizz-video-effects/LICENSE-motionmaxxing.txt +202 -0
- package/src/skills-lib/wizz-video-effects/SKILL.md +5 -2
- package/src/skills-lib/wizz-video-effects/assets/chrome/collect_layout.js +113 -0
- package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +35 -0
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +30 -1
- package/src/skills-lib/wizz-video-effects/references/gates.md +3 -1
- package/src/skills-lib/wizz-video-effects/scripts/check_chrome.py +376 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_dead_frames.py +193 -0
- package/src/skills-lib/wizz-video-effects/scripts/config.py +13 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_chrome.py +181 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_dead_frames.py +171 -0
- package/src/skills-lib/wizz-video-styles/SKILL.md +6 -2
- package/src/skills-lib/wizz-video-styles/references/motion-laws.md +62 -0
- package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +1 -0
- package/src/skills-lib/wizz-video-styles/scripts/blind_review.py +159 -0
- package/src/skills-lib/wizz-video-styles/scripts/taste.mjs +110 -0
- package/src/skills-lib/wizz-video-styles/scripts/test_blind_review.py +82 -0
- package/src/skills-lib/wizz-vsl/LICENSE +49 -0
- package/src/skills-lib/wizz-vsl/SKILL.md +122 -0
- package/src/skills-lib/wizz-vsl/references/assembly-qa.md +123 -0
- package/src/skills-lib/wizz-vsl/references/blocks-and-leads.md +149 -0
- package/src/skills-lib/wizz-vsl/references/diagnosis.md +95 -0
- package/src/skills-lib/wizz-vsl/references/intake.md +200 -0
- package/src/skills-lib/wizz-vsl/references/mechanisms.md +138 -0
- package/src/skills-lib/wizz-vsl/references/niches.md +40 -0
- package/src/skills-lib/wizz-vsl/references/offer.md +91 -0
- package/src/skills-lib/wizz-vsl/references/production.md +75 -0
- package/tools/hooks/README.md +10 -9
- package/tools/hooks/wizz-arrumar.js +1 -0
- package/tools/hooks/wizz-artifact-guard.js +31 -5
- package/tools/hooks/wizz-cerebro-lib.js +36 -13
- package/tools/hooks/wizz-feature-lib.js +4 -1
- package/tools/hooks/wizz-feature.js +66 -76
- package/tools/hooks/wizz-sprint-archive-lib.js +311 -0
- package/tools/installer/core/hooks-settings.js +1 -0
- package/tools/installer/core/installer.js +6 -0
- package/tools/installer/modules/core-seed.js +72 -0
- package/tools/installer/modules/mcp-config.js +3 -0
- package/tools/installer/ui.js +16 -26
package/tools/hooks/README.md
CHANGED
|
@@ -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
|
|
20
|
-
|
|
|
21
|
-
| `wizz-feature-lib.js`
|
|
22
|
-
| `wizz-layout-lib.js`
|
|
23
|
-
| `wizz-cerebro-lib.js`
|
|
24
|
-
| `wizz-debug-lib.js`
|
|
25
|
-
| `wizz-
|
|
26
|
-
| `wizz-
|
|
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
|
|
|
@@ -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 ${
|
|
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 &&
|
|
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 ${
|
|
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
|
|
99
|
-
const problems = lib.frontmatterProblems(content,
|
|
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:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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 >
|
|
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 = [
|
|
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`
|
|
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
|
-
|
|
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)) ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 (
|
|
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,
|
|
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(' ') };
|