wizz-method 2.19.2 → 2.20.1
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/package.json +1 -1
- package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +2 -2
- package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
- package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
- package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
- package/src/core-skills/_shared/handoff-protocol.md +2 -1
- package/src/modules/wizz/_shared/organizacao.md +17 -10
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/modules/wizz/agents/wizz-maestro/customize.toml +1 -1
- package/src/skills-lib/wizz-router/SKILL.md +3 -2
- package/tools/hooks/session-rules.js +4 -4
- package/tools/hooks/wizz-artifact-guard.js +5 -4
- package/tools/hooks/wizz-cerebro-lib.js +5 -1
- package/tools/hooks/wizz-feature-lib.js +40 -0
- package/tools/hooks/wizz-feature.js +126 -24
- package/tools/hooks/wizz-router-enforce.js +2 -2
- package/tools/installer/core/installer.js +7 -1
- package/tools/installer/core/module-config-writer.js +64 -7
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@ activation_steps_append = []
|
|
|
39
39
|
# (the Wizz block imports project-context.md there once per session). Add
|
|
40
40
|
# `file:{project-root}/**/project-context.md` here only to load it on every activation.
|
|
41
41
|
persistent_facts = [
|
|
42
|
-
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a
|
|
42
|
+
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a new subject goes into the open sprint (the active feature), sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a brief's `feature:` field wins. Create artifacts with `wizz-feature.js artefato <tipo>` so they land as <tipo>-<slug>.md in {planning_artifacts}/{active_feature}/ or {implementation_artifacts}/{active_feature}/ with valid frontmatter; project-wide artifacts (architecture, context) stay at the root. Owner decisions go in with `wizz-feature.js decidir`. Memory saved in this session carries the same slug as its theme. A question or conversation records nothing. Full rule: wizz module _shared/organizacao.md.",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
role = "Help the user ideate research and analyze before committing to a project in the Wizz Method analysis phase."
|
|
@@ -27,7 +27,7 @@ Invoke this skill only for the explicit cases in "When this skill runs" below.
|
|
|
27
27
|
3. **The request itself.** When 1 and 2 are both empty and the request is named work (a feature, a batch, a screen, a page, a campaign, a bug with a scope: "vamos fazer o painel de assinaturas"). Derive the slug from the subject, 2 to 4 words, and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE dispatching; say so in **one line**. Do not ask. This step is mandatory for the router and the maestro, not a nice-to-have: skipping it is how a project spends its whole life with an empty `active_feature`.
|
|
28
28
|
4. **Nothing.** Only for work that is not named (a question, a conversation, a one-line tweak with no artifact). Artifacts go to the roots and memory carries no feature tag.
|
|
29
29
|
|
|
30
|
-
**
|
|
30
|
+
**The open sprint rules.** The active feature is the open sprint: every request goes into it until it is closed, with no new folder. A new subject goes into it, sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask the owner once: add to this sprint or open another. `definir <other>` with a sprint open is refused unless `--novo` (the owner's choice). **Close** when the delivery closes (deploy or acceptance): `wizz-feature.js verificar` must pass, then `wizz-feature.js encerrar --cerebro [--proxima <slug>]` marks the spec `no-ar`, moves the sprint's folders (and the root `sprint-status.yaml`) to `_arquivo/<slug>/`, clears or switches `active_feature`, and writes the Cérebro (state + one Sessions line; without a vault it prints the instruction instead).
|
|
31
31
|
|
|
32
32
|
**Optional branch.** `feature_branches: auto` in `_wizz/bmm/config.yaml` makes `definir` (and the SessionStart hook) propose `git switch -c feat/<slug>` when you are on a trunk branch of a git repo. The default is `off`, for whoever works directly on `main`.
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ Invoke this skill only for the explicit cases in "When this skill runs" below.
|
|
|
36
36
|
| Command | What it does |
|
|
37
37
|
|---|---|
|
|
38
38
|
| `resolver` | Active feature and its source (branch, config, none). |
|
|
39
|
-
| `definir <slug or name
|
|
39
|
+
| `definir <slug or name> [--novo]` | Normalizes, writes `active_feature`, creates both subfolders, prints one line. With another sprint open it refuses unless `--novo` (owner's choice). |
|
|
40
40
|
| `artefato <tipo>` | Creates `<tipo>-<slug>.md` (spec, plano, auditoria, story, review, roteiro) in the right folder with valid frontmatter. |
|
|
41
41
|
| `decidir "<texto>"` | Appends `D-NN` with the date to the feature's spec and to its `decisoes:` frontmatter. |
|
|
42
42
|
| `status <status>` | Sets the spec's status: `rascunho`, `aprovado`, `implementado` or `no-ar` (it writes; to only read, use `resolver` or `listar`). |
|
|
@@ -39,7 +39,7 @@ activation_steps_append = []
|
|
|
39
39
|
# (the Wizz block imports project-context.md there once per session). Add
|
|
40
40
|
# `file:{project-root}/**/project-context.md` here only to load it on every activation.
|
|
41
41
|
persistent_facts = [
|
|
42
|
-
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a
|
|
42
|
+
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a new subject goes into the open sprint (the active feature), sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a brief's `feature:` field wins. Create artifacts with `wizz-feature.js artefato <tipo>` so they land as <tipo>-<slug>.md in {planning_artifacts}/{active_feature}/ or {implementation_artifacts}/{active_feature}/ with valid frontmatter; project-wide artifacts (architecture, context) stay at the root. Owner decisions go in with `wizz-feature.js decidir`. Memory saved in this session carries the same slug as its theme. A question or conversation records nothing. Full rule: wizz module _shared/organizacao.md.",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
role = "Translate product vision into a validated PRD, epics, and stories that development can execute during the Wizz Method planning phase."
|
|
@@ -39,7 +39,7 @@ activation_steps_append = []
|
|
|
39
39
|
# (the Wizz block imports project-context.md there once per session). Add
|
|
40
40
|
# `file:{project-root}/**/project-context.md` here only to load it on every activation.
|
|
41
41
|
persistent_facts = [
|
|
42
|
-
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a
|
|
42
|
+
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a new subject goes into the open sprint (the active feature), sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a brief's `feature:` field wins. Create artifacts with `wizz-feature.js artefato <tipo>` so they land as <tipo>-<slug>.md in {planning_artifacts}/{active_feature}/ or {implementation_artifacts}/{active_feature}/ with valid frontmatter; project-wide artifacts (architecture, context) stay at the root. Owner decisions go in with `wizz-feature.js decidir`. Memory saved in this session carries the same slug as its theme. A question or conversation records nothing. Full rule: wizz module _shared/organizacao.md.",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
role = "Turn user needs and the PRD into UX design specifications that inform architecture and implementation during the Wizz Method planning phase."
|
|
@@ -39,7 +39,7 @@ activation_steps_append = []
|
|
|
39
39
|
# (the Wizz block imports project-context.md there once per session). Add
|
|
40
40
|
# `file:{project-root}/**/project-context.md` here only to load it on every activation.
|
|
41
41
|
persistent_facts = [
|
|
42
|
-
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a
|
|
42
|
+
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a new subject goes into the open sprint (the active feature), sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a brief's `feature:` field wins. Create artifacts with `wizz-feature.js artefato <tipo>` so they land as <tipo>-<slug>.md in {planning_artifacts}/{active_feature}/ or {implementation_artifacts}/{active_feature}/ with valid frontmatter; project-wide artifacts (architecture, context) stay at the root. Owner decisions go in with `wizz-feature.js decidir`. Memory saved in this session carries the same slug as its theme. A question or conversation records nothing. Full rule: wizz module _shared/organizacao.md.",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
role = "Convert the PRD and UX into technical architecture decisions that keep implementation on track during the Wizz Method solutioning phase."
|
|
@@ -39,7 +39,7 @@ activation_steps_append = []
|
|
|
39
39
|
# (the Wizz block imports project-context.md there once per session). Add
|
|
40
40
|
# `file:{project-root}/**/project-context.md` here only to load it on every activation.
|
|
41
41
|
persistent_facts = [
|
|
42
|
-
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a
|
|
42
|
+
"FEATURE: the hooks already inject the active feature ('FEATURE ATIVA: <slug>' or 'Feature: <slug>') or 'nenhuma ativa'. Named work with no active feature: derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE writing anything, announcing it in ONE line (never ask for the slug); a new subject goes into the open sprint (the active feature), sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a brief's `feature:` field wins. Create artifacts with `wizz-feature.js artefato <tipo>` so they land as <tipo>-<slug>.md in {planning_artifacts}/{active_feature}/ or {implementation_artifacts}/{active_feature}/ with valid frontmatter; project-wide artifacts (architecture, context) stay at the root. Owner decisions go in with `wizz-feature.js decidir`. Memory saved in this session carries the same slug as its theme. A question or conversation records nothing. Full rule: wizz module _shared/organizacao.md.",
|
|
43
43
|
"When implementing against a library or framework, consult Context7 for current docs, APIs, and version-specific syntax before relying on training data or web search: call resolve-library-id with the library name, then query-docs with the returned /org/project id and a specific question. If Context7 is unavailable, tell the user once that they can enable it with `npx ctx7 setup`, then fall back to web research.",
|
|
44
44
|
]
|
|
45
45
|
|
|
@@ -12,7 +12,8 @@ and pass along only the skill's entrypoint and the references actually needed
|
|
|
12
12
|
- area skill: canonical name of the persona to invoke via `Skill` (e.g. `wizz-growth`). Not a `subagent_type`: to isolate the task, pick an available native executor (`wizz-exec-*`) and instruct it to load that skill before working. The absence of an executor doesn't block invoking the skill in the current session.
|
|
13
13
|
- feature: the active feature slug, resolved BEFORE the handoff (in a Wizz
|
|
14
14
|
project: `node _wizz/hooks/wizz-feature.js resolver`; named work with no
|
|
15
|
-
feature gets one with `definir <slug from the request>`, without asking
|
|
15
|
+
feature gets one with `definir <slug from the request>`, without asking;
|
|
16
|
+
with a feature active, it is the open sprint and new work goes into it).
|
|
16
17
|
`feature: nenhuma` is valid only for work that is not named (a question, a
|
|
17
18
|
conversation). Receiver rule: a brief WITHOUT this field is resolved by the
|
|
18
19
|
receiver itself with the same steps before it writes anything; it never
|
|
@@ -7,27 +7,34 @@ Every step below is a **command**, not a memory exercise. The commands live in t
|
|
|
7
7
|
- `F` = `node _wizz/hooks/wizz-feature.js`
|
|
8
8
|
- `A` = `node _wizz/hooks/wizz-arrumar.js`
|
|
9
9
|
|
|
10
|
-
`F` subcommands: `resolver` (active feature and its source), `definir <slug
|
|
10
|
+
`F` subcommands: `resolver` (active feature and its source), `definir <slug> [--novo]`, `artefato <tipo>`, `decidir "<texto>"`, `status <status>` (sets the spec status), `verificar`, `encerrar` (closes and archives the sprint), `listar` (open sprints, plus a count of the archived ones). `resolver` and `listar` only read. The `wizz-set-feature` skill is just the manual override over `F definir` (rename, switch or clear on the owner's explicit request); nobody invokes it for the normal flow.
|
|
11
11
|
|
|
12
|
-
## 1.
|
|
12
|
+
## 1. The open sprint rules
|
|
13
13
|
|
|
14
|
-
The
|
|
14
|
+
**The active feature is the open sprint.** One sprint is open at a time, and every piece of work goes into it until it is closed. A new subject does **not** create a new feature or a new folder on its own: dozens of half-used folders is how the Lucrare ended up with 13 features (owner's decision, 2026-10-15).
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
The SessionStart and UserPromptSubmit hooks already tell you the state (`FEATURE ATIVA: <slug>`, `Sprint aberto: <slug>` or `nenhuma ativa`). Before you dispatch anything or write any artifact:
|
|
17
|
+
|
|
18
|
+
1. **Work and no open sprint** (a feature, a batch, a screen, a page, a campaign, a bug with a scope) → derive the slug from the **subject** of the request, 2 to 4 words in kebab-case, never from the verb ("vamos fazer o painel de assinaturas" → painel-assinaturas). Run `F definir <slug>` and tell the owner in **one line**. Never ask for a slug that comes out of the request.
|
|
19
|
+
2. **Open sprint, same subject or continuation** ("agora ajusta o filtro do painel") → keep it. Write nothing.
|
|
20
|
+
3. **Open sprint, new subject that fits its goal** (same product, same objective) → it goes into the open sprint, sized by the work. No `definir`, no new folder, one line to the owner:
|
|
21
|
+
- **small** (a colour, a text, one adjustment, no planning) → just do it; no story;
|
|
22
|
+
- **medium** (several steps, needs testing and review) → a story in the open sprint;
|
|
23
|
+
- **large** (a new objective inside the same product) → an epic with its stories.
|
|
24
|
+
4. **Open sprint, new subject that clearly does not fit** (a separate objective, another product) → the only case where you ask, **once**: "add to `<slug>` or open a new sprint?". Only after the owner chooses a new one: `F definir <new-slug> --novo`. Without `--novo`, `definir` refuses while a sprint is open.
|
|
25
|
+
5. **Not work** (a question, conversation, a trivial one-line tweak that produces no artifact) → do nothing.
|
|
26
|
+
6. **Feature branch** (`feat/<slug>`) → the branch already is the sprint; when the config disagrees, `F definir <branch-slug> --novo`.
|
|
22
27
|
7. **`feature_branches: auto`** in the config and you are on a trunk branch → propose `git switch -c feat/<slug>` in one line. With `off` (the default, for whoever works on `main`) say nothing about branches.
|
|
23
28
|
|
|
29
|
+
**Closing archives.** `F encerrar` moves the sprint's two folders to `_arquivo/<slug>/` inside each artifact folder, together with the root `{implementation_artifacts}/sprint-status.yaml`, so the next sprint starts clean. `F listar` shows the open sprints and one line with the number of archived ones. The archive is history, not trash: when a request needs a past decision, spec or retro ("how did we solve the Meta sync?"), search `_arquivo/` of both artifact folders (and the Cérebro, which points to it) before saying it does not exist.
|
|
30
|
+
|
|
24
31
|
**Handoff:** every brief carries `feature: <slug>` (or `feature: nenhuma` for work that is not named). A receiver without that field resolves it itself with the steps above before writing anything; it never writes an artifact without a resolved feature.
|
|
25
32
|
|
|
26
33
|
## 2. Artifacts in the right place
|
|
27
34
|
|
|
28
35
|
- Create planning and implementation artifacts with `F artefato <tipo>` (`spec`, `plano`, `auditoria`, `story`, `review`, `roteiro`). It writes `<tipo>-<slug>.md` under `{planning_artifacts}/{feature}/` or `{implementation_artifacts}/{feature}/` (the `roteiro` goes to `{project_knowledge}/`), with the frontmatter already valid: `feature`, `status` (`rascunho|aprovado|implementado|no-ar`), `atualizado_em` (`YYYY-MM-DD`), `decisoes` (`[D-..]`).
|
|
29
36
|
- Project-wide artifacts (architecture, project context) stay at the root of the artifact folders.
|
|
30
|
-
- The `wizz-artifact-guard.js` hook warns after a Write that lands outside the active feature's folder or without a feature, and after a Write or Edit that leaves the frontmatter broken. **Fix it at once** (move the file, run `F definir
|
|
37
|
+
- The `wizz-artifact-guard.js` hook warns after a Write that lands outside the active feature's folder or without a feature, and after a Write or Edit that leaves the frontmatter broken. **Fix it at once** (move the file into the open sprint's folder, fix the frontmatter; run `F definir` only when no sprint is open); never leave the warning for later.
|
|
31
38
|
|
|
32
39
|
## 3. Project root organized from the start
|
|
33
40
|
|
|
@@ -69,7 +69,7 @@ If the user already stated the intent, **classify and dispatch directly** (see R
|
|
|
69
69
|
|
|
70
70
|
## Feature and organization (before routing, every time)
|
|
71
71
|
|
|
72
|
-
Before you route, dispatch or write anything, resolve the feature with the steps in [organizacao.md](../../_shared/organizacao.md) section 1: named work with no active feature → `node _wizz/hooks/wizz-feature.js definir <slug from the request>` and **one line** to the owner (never ask for the slug); a
|
|
72
|
+
Before you route, dispatch or write anything, resolve the feature with the steps in [organizacao.md](../../_shared/organizacao.md) section 1: named work with no active feature → `node _wizz/hooks/wizz-feature.js definir <slug from the request>` and **one line** to the owner (never ask for the slug); the active feature is the open sprint: a new subject goes into it, sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it. With no `layout:` in the config, propose and record it (`node _wizz/hooks/wizz-arrumar.js layout`, then `layout --gravar`) in the same single line. Every brief you hand over carries `feature:`.
|
|
73
73
|
|
|
74
74
|
While the sequence runs you keep the **documentation duty** (section 4 of the same file) without being asked: scope approved → spec with acceptance criteria; each owner decision → `wizz-feature.js decidir`; implementation done → the spec's `## Implementação`; going live → roteiro with rollback in `{project_knowledge}/`. Closing a feature's delivery runs the checklist (`wizz-feature.js verificar`, section 5): **you do not close the sequence with a pending item.**
|
|
75
75
|
|
|
@@ -23,7 +23,7 @@ include = [
|
|
|
23
23
|
|
|
24
24
|
persistent_facts = [
|
|
25
25
|
"Chaining mode: AUTOMATIC. I announce the full sequence at the start and run the agents in order, without stopping between them. I only pause for a business decision that belongs to the user or an irreversible risk (deploy, delete, spending).",
|
|
26
|
-
"Organization duty (before dispatch, without being asked): named work with no active feature -> `node _wizz/hooks/wizz-feature.js definir <slug from the request>` and one line;
|
|
26
|
+
"Organization duty (before dispatch, without being asked): named work with no active feature -> `node _wizz/hooks/wizz-feature.js definir <slug from the request>` and one line; new subject -> enters the open sprint while it fits (small tweak: just do it; multi-step: story; new objective: epic); does not fit -> ask once: add here or open another (`definir <new> --novo` only after the owner chooses); question or conversation -> nothing. Every handoff carries `feature:`. Scope approved -> spec with acceptance criteria; owner decision -> `decidir`; implemented -> Implementação section; live -> roteiro + rollback; closing -> `verificar` must pass before `encerrar`. I never close a sequence with a pending item. Details in _shared/organizacao.md.",
|
|
27
27
|
"Planning gate: on an EXECUTION request where the 'needs planning' factor = YES and there's no matching PRD/story/brief, I ask once whether to create the artifact first (recommended) or skip it; once the gate is resolved, the chain proceeds automatically. Details in _shared/planning-gate.md.",
|
|
28
28
|
"I speak easy {communication_language}, short sentences, no jargon. I summarize as if explaining to a client.",
|
|
29
29
|
"Full details of the Wizz layer are in _shared/ (communication-rules.md, encerramento.md, token-economy.md, cerebro.md) at the root of the wizz module.",
|
|
@@ -12,7 +12,8 @@ Invoke this skill BEFORE acting on non-trivial requests (feature, bug, design, m
|
|
|
12
12
|
|
|
13
13
|
**Single exception:** 1-line edits (typo, rename) and pure conversation. **Objective criterion (TRIVIALITY GATE):** a request touching ≤1 file, ~≤10 lines, no new behavior, a known type of change → execute directly, without triggering the router/maestro.
|
|
14
14
|
|
|
15
|
-
**Outside Plan Mode:** list candidates via `AskUserQuestion` and wait for confirmation BEFORE firing. Never auto-fire
|
|
15
|
+
**Outside Plan Mode, outside a Wizz project (section B):** list candidates via `AskUserQuestion` and wait for confirmation BEFORE firing. Never auto-fire there.
|
|
16
|
+
**Inside a Wizz project (section A):** do NOT ask to confirm the dispatch. Resolve the feature, then hand off at once; the only questions you may ask are the ambiguous-referent gate below and, with a sprint open, "add to this sprint or open another" when the new subject does not fit it.
|
|
16
17
|
**Inside Plan Mode:** auto-fire every relevant one to enrich the plan.
|
|
17
18
|
</EXTREMELY-IMPORTANT>
|
|
18
19
|
|
|
@@ -89,7 +90,7 @@ This gate does NOT fire for an open-scope request. "My site is bad, sort it out"
|
|
|
89
90
|
- *1 area, but 2+ factors → maestro:* `m06` ("infra refactor: database, APIs, deploy, monitoring"): 1 area (dev), but multi-step + needs planning + high-risk architecture → `wizz-maestro` even without crossing areas.
|
|
90
91
|
- *2+ areas, each part light → still maestro (clause 1):* e.g. "adjust the secondary button's color AND change one word in the CTA": each isolated change is trivial, but it crosses 2 areas (designer + copy) in the same request, so it falls under clause 1 and goes to the maestro even without heavy multi-step work. The current dataset has no equivalent case (`m01`-`m12` always combine 2+ areas with high complexity); it's the calibration gap flagged by finding M1.
|
|
91
92
|
|
|
92
|
-
**Feature before delegating (mandatory, verifiable):** named work (a feature, a batch, a screen, a page, a campaign, a bug with a scope) with no active feature → derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE handing off, with one line to the owner and no question; a
|
|
93
|
+
**Feature before delegating (mandatory, verifiable):** named work (a feature, a batch, a screen, a page, a campaign, a bug with a scope) with no active feature → derive the slug from the request's subject and run `node _wizz/hooks/wizz-feature.js definir <slug>` BEFORE handing off, with one line to the owner and no question; with a feature active, it is the open sprint and the work goes into it: a new subject goes into it, sized by the work (a small tweak is just done, no story; a multi-step change becomes a story; a new objective becomes an epic), while it fits the sprint's goal; only if it does not fit, ask ONCE: add to this sprint or open another (`definir <new> --novo` only after the owner chooses); a continuation keeps it; a question or conversation writes nothing. Full rule in the wizz module's `_shared/organizacao.md`, section 1.
|
|
93
94
|
|
|
94
95
|
When delegating (to the maestro or to the area agent), state the brief in the format of the [shared handoff protocol](../../core-skills/_shared/handoff-protocol.md): `feature: <slug>` (or `feature: nenhuma`), `origem: wizz-router` (anti-loop — whoever receives it never sends the same request back to you), a cerebro summary if one has already been consulted, decisions already made, and the relevant section of the skill. Also include the **verdict on the 4 factors** — especially `precisa planejar: sim/não` — so the receiver can apply the Planning Gate (wizz module, `_shared/planning-gate.md`) without re-deriving it: with the planning factor set to YES and no corresponding PRD/story/brief, the receiver asks once whether to create the artifact before executing; once the gate is resolved, the chain proceeds automatically.
|
|
95
96
|
|
|
@@ -87,8 +87,8 @@ function wizzReminders(state) {
|
|
|
87
87
|
const out = [];
|
|
88
88
|
if (!state.slug) {
|
|
89
89
|
out.push(
|
|
90
|
-
`NENHUMA FEATURE ATIVA: o primeiro pedido
|
|
91
|
-
`Antes de despachar, derive o slug do pedido e grave: ${HOOK_CMD} definir <slug>. Sem perguntar; uma linha de confirmação.`,
|
|
90
|
+
`NENHUMA FEATURE ATIVA (nenhum sprint aberto): o primeiro pedido de trabalho (feature, lote, tela, bug com escopo) abre um. ` +
|
|
91
|
+
`Antes de despachar, derive o slug do pedido e grave: ${HOOK_CMD} definir <slug>. Sem perguntar; uma linha de confirmação. Pergunta ou conversa não abre nada.`,
|
|
92
92
|
);
|
|
93
93
|
} else if (state.featureBranches === 'auto' && state.git.isRepo && state.onTrunk) {
|
|
94
94
|
out.push(
|
|
@@ -120,7 +120,7 @@ function buildFeatureContext(cwd) {
|
|
|
120
120
|
|
|
121
121
|
if (active) {
|
|
122
122
|
parts.push(
|
|
123
|
-
`FEATURE ATIVA: ${active}` + (fromBranch ? ' (
|
|
123
|
+
`FEATURE ATIVA: ${active}` + (fromBranch ? ' (sprint aberto, derivado do branch git).' : ' (sprint aberto, do config).'),
|
|
124
124
|
'Use como contexto sem perguntar nada:',
|
|
125
125
|
state.isWizz
|
|
126
126
|
? `artefatos de planejamento e implementação vão para as subpastas ${active}/ com nome <tipo>-<slug>.md (${HOOK_CMD} artefato <tipo>);`
|
|
@@ -133,7 +133,7 @@ function buildFeatureContext(cwd) {
|
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
parts.push(
|
|
136
|
-
'NÃO peça confirmação de feature.
|
|
136
|
+
'NÃO peça confirmação de feature. O sprint aberto manda: todo trabalho entra nele até ser encerrado, sem pasta nova. Assunto novo que cabe no objetivo do sprint entra pelo tamanho: ajuste pequeno só faz (sem story), vários passos vira story, objetivo novo vira épico; avise em uma linha. Histórico de sprints fechados fica em _arquivo/ das pastas de artefato: procure lá quando precisar. Só se NÃO couber, pergunte UMA vez: adicionar neste sprint ou abrir outro (definir <novo> --novo, só com a escolha do dono). Pergunta ou conversa não grava nada.',
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
139
|
if (state.isWizz) parts.push(...wizzReminders(state));
|
|
@@ -87,15 +87,16 @@ function artifactWarning(file, content, state, { frontmatterOnly = false } = {})
|
|
|
87
87
|
`Grave a feature a partir do pedido (node ${HOOK_CMD}/wizz-feature.js definir <slug>) e mova o arquivo para a subpasta dela.`,
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
|
-
} else if (!frontmatterOnly && slug && parts[0] !== slug) {
|
|
90
|
+
} else if (!frontmatterOnly && slug && parts[0] !== slug && parts[0] !== lib.ARCHIVE_DIR) {
|
|
91
91
|
msgs.push(
|
|
92
|
-
`ARTEFATO EM
|
|
93
|
-
`
|
|
92
|
+
`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).`,
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if (type) {
|
|
98
|
-
const
|
|
98
|
+
const owner = parts[0] === lib.ARCHIVE_DIR ? parts[1] : parts[0];
|
|
99
|
+
const problems = lib.frontmatterProblems(content, parts.length > 1 ? owner : slug);
|
|
99
100
|
if (problems.length > 0) msgs.push(`FRONTMATTER de ${relFile}: ${problems.join('; ')}.`);
|
|
100
101
|
}
|
|
101
102
|
return msgs.join(' ');
|
|
@@ -213,7 +213,11 @@ function recordClose({ root, slug, status, projeto }) {
|
|
|
213
213
|
|
|
214
214
|
const date = lib.today();
|
|
215
215
|
const dirs = lib.artifactDirs(root);
|
|
216
|
-
|
|
216
|
+
// O `encerrar` arquiva o sprint antes de chamar aqui: procura nas duas casas.
|
|
217
|
+
const spec =
|
|
218
|
+
[path.join(dirs.planning, lib.ARCHIVE_DIR, slug), path.join(dirs.planning, slug)]
|
|
219
|
+
.map((d) => path.join(d, `spec-${slug}.md`))
|
|
220
|
+
.find((f) => fs.existsSync(f)) || path.join(dirs.planning, slug, `spec-${slug}.md`);
|
|
217
221
|
const specText = fs.existsSync(spec) ? fs.readFileSync(spec, 'utf8') : '';
|
|
218
222
|
const specRel = path.relative(root, spec);
|
|
219
223
|
const resumo = implementationSummary(specText);
|
|
@@ -41,6 +41,9 @@ const PROJECT_WIDE = /^(architecture|arquitetura|project-context|contexto|sprint
|
|
|
41
41
|
|
|
42
42
|
const CONFIG_REL = path.join('_wizz', 'bmm', 'config.yaml');
|
|
43
43
|
|
|
44
|
+
// Arquivo morto dos sprints fechados, dentro de cada pasta de artefato.
|
|
45
|
+
const ARCHIVE_DIR = '_arquivo';
|
|
46
|
+
|
|
44
47
|
/**
|
|
45
48
|
* Normaliza texto livre para slug kebab-case: o mesmo formato que a skill
|
|
46
49
|
* `wizz-set-feature` grava, para branch e config nunca divergirem.
|
|
@@ -384,12 +387,48 @@ function sectionContent(text, title) {
|
|
|
384
387
|
.trim();
|
|
385
388
|
}
|
|
386
389
|
|
|
390
|
+
// Link markdown relativo: `](alvo)` ou `](alvo#ancora)`, sem esquema nem âncora pura.
|
|
391
|
+
const MD_LINK = /\]\((?!(?:[a-z][\w+.-]*:|#|\/))([^)\s#?]+)([^)\s]*)\)/gi;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Reescreve os links markdown relativos de um arquivo que mudou de pasta.
|
|
395
|
+
* Cada alvo é resolvido a partir da pasta ANTIGA, passa por `mapAbs` (o alvo
|
|
396
|
+
* também pode ter sido movido) e volta relativo à pasta NOVA. Sem mudança
|
|
397
|
+
* devolve o mesmo texto; link quebrado (alvo inexistente) fica intocado.
|
|
398
|
+
* @param {string} text
|
|
399
|
+
* @param {string} oldDir - pasta antiga do arquivo (absoluta)
|
|
400
|
+
* @param {string} newDir - pasta nova do arquivo (absoluta)
|
|
401
|
+
* @param {(abs: string) => string} [mapAbs] - caminho atual de um alvo movido
|
|
402
|
+
* @returns {string}
|
|
403
|
+
*/
|
|
404
|
+
function rebaseMarkdownLinks(text, oldDir, newDir, mapAbs = (p) => p) {
|
|
405
|
+
return String(text).replaceAll(MD_LINK, (whole, target, rest) => {
|
|
406
|
+
let decoded;
|
|
407
|
+
try {
|
|
408
|
+
decoded = decodeURI(target);
|
|
409
|
+
} catch {
|
|
410
|
+
return whole;
|
|
411
|
+
}
|
|
412
|
+
const original = path.resolve(oldDir, decoded);
|
|
413
|
+
const abs = mapAbs(original);
|
|
414
|
+
// Link que já não apontava para nada fica como está: reescrever inventaria outro caminho.
|
|
415
|
+
if (!fs.existsSync(original) && !fs.existsSync(abs)) return whole;
|
|
416
|
+
const next = path.relative(newDir, abs).split(path.sep).join('/') || '.';
|
|
417
|
+
if (next === decoded) return whole;
|
|
418
|
+
// Mantém o estilo do link: cru fica cru; com acento cru, só o espaço é codificado.
|
|
419
|
+
let encoded = next;
|
|
420
|
+
if (target !== decoded) encoded = /[\u0080-\uFFFF]/.test(target) ? next.replaceAll(' ', '%20') : encodeURI(next);
|
|
421
|
+
return `](${encoded}${rest})`;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
387
425
|
module.exports = {
|
|
388
426
|
TRUNK,
|
|
389
427
|
ARTIFACT_TYPES,
|
|
390
428
|
STATUS,
|
|
391
429
|
PROJECT_WIDE,
|
|
392
430
|
CONFIG_REL,
|
|
431
|
+
ARCHIVE_DIR,
|
|
393
432
|
toSlug,
|
|
394
433
|
today,
|
|
395
434
|
findUp,
|
|
@@ -411,4 +450,5 @@ module.exports = {
|
|
|
411
450
|
frontmatterProblems,
|
|
412
451
|
artifactTypeOf,
|
|
413
452
|
sectionContent,
|
|
453
|
+
rebaseMarkdownLinks,
|
|
414
454
|
};
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
// Spider: a regra "derive do pedido" dependia de memória do agente).
|
|
8
8
|
//
|
|
9
9
|
// resolver feature ativa e de onde veio (branch > config > nada)
|
|
10
|
-
// definir <slug|texto> grava active_feature e cria as subpastas
|
|
10
|
+
// definir <slug|texto> abre o sprint: grava active_feature e cria as subpastas
|
|
11
|
+
// (sem perguntar). Com sprint aberto e outro slug, recusa:
|
|
12
|
+
// o trabalho novo entra no sprint aberto; abrir outro só
|
|
13
|
+
// com a escolha do dono (--novo)
|
|
11
14
|
// artefato <tipo> cria <tipo>-<slug>.md com frontmatter no lugar certo
|
|
12
15
|
// decidir "<texto>" registra D-NN na spec da feature, na hora da decisão
|
|
13
16
|
// status <status> rascunho|aprovado|implementado|no-ar na spec
|
|
14
17
|
// verificar checklist de fechamento (exit 1 se houver pendência)
|
|
15
|
-
// encerrar status final na spec,
|
|
18
|
+
// encerrar status final na spec, arquiva o sprint em _arquivo/
|
|
19
|
+
// (com o sprint-status.yaml da raiz), limpa/troca active_feature
|
|
16
20
|
// (recusa com pendência do checklist, salvo --forcar);
|
|
17
21
|
// com --cerebro grava Estado + linha de Sessões no
|
|
18
22
|
// projetos/<projeto>.md do Cérebro (--projeto <nome>)
|
|
19
|
-
// listar
|
|
23
|
+
// listar sprints abertos e arquivados, com o status de cada spec
|
|
20
24
|
//
|
|
21
25
|
// Opções: --json (saída para máquina), --cwd <dir>, --feature <slug>.
|
|
22
26
|
// Fail-open: fora de projeto Wizz, avisa e sai com 0 sem escrever nada.
|
|
@@ -143,6 +147,16 @@ function cmdDefinir(state, opts) {
|
|
|
143
147
|
if (!slug) return { ok: false, code: 2, line: 'Uso: definir <slug ou nome da feature>. Nada foi gravado.' };
|
|
144
148
|
const { root } = state;
|
|
145
149
|
const previous = state.fromConfig;
|
|
150
|
+
// O sprint aberto manda: assunto novo entra nele. Abrir outro sprint com um
|
|
151
|
+
// aberto é escolha do dono, nunca do agente (pastas demais na Lucrare, 2026-10-15).
|
|
152
|
+
if (previous && previous !== slug && !opts.novo) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
code: 1,
|
|
156
|
+
data: { feature: previous, recusado: slug },
|
|
157
|
+
line: `Sprint aberto: ${previous}. O pedido novo entra nele pelo tamanho (ajuste pequeno só faz; vários passos, story; objetivo novo, épico). Se não couber, pergunte ao dono uma vez: adicionar em ${previous} ou abrir sprint novo? Só com a escolha dele: definir ${slug} --novo. Nada foi gravado.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
146
160
|
lib.writeConfigValue(root, 'active_feature', slug);
|
|
147
161
|
const dirs = lib.artifactDirs(root);
|
|
148
162
|
for (const d of [dirs.planning, dirs.implementation]) fs.mkdirSync(path.join(d, slug), { recursive: true });
|
|
@@ -150,7 +164,8 @@ function cmdDefinir(state, opts) {
|
|
|
150
164
|
const parts = [
|
|
151
165
|
`Feature: ${slug}. Artefatos em ${rel(root, path.join(dirs.planning, slug))}/ e ${rel(root, path.join(dirs.implementation, slug))}/.`,
|
|
152
166
|
];
|
|
153
|
-
if (previous && previous !== slug)
|
|
167
|
+
if (previous && previous !== slug)
|
|
168
|
+
parts.push(`Sprint novo a pedido do dono: ${previous} → ${slug} (${previous} segue aberto até ser encerrado).`);
|
|
154
169
|
if (state.fromBranch && state.fromBranch !== slug)
|
|
155
170
|
parts.push(
|
|
156
171
|
`Atenção: o branch ${state.git.branch} aponta para ${state.fromBranch} e ganha do config. Troque de branch se ${slug} é outro trabalho.`,
|
|
@@ -246,6 +261,72 @@ function looseArtifacts(root, dirs) {
|
|
|
246
261
|
return out;
|
|
247
262
|
}
|
|
248
263
|
|
|
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
|
+
|
|
249
330
|
/**
|
|
250
331
|
* Checklist de fechamento. Itens mecânicos são checados; os de julgamento
|
|
251
332
|
* (Cérebro, memória) voltam como "confirmar" para o agente responder.
|
|
@@ -329,15 +410,18 @@ function cmdEncerrar(state, opts) {
|
|
|
329
410
|
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).`,
|
|
330
411
|
};
|
|
331
412
|
}
|
|
332
|
-
|
|
413
|
+
setSpecStatus(state, slug, status);
|
|
414
|
+
const archived = archiveSprint(state.root, slug);
|
|
415
|
+
const file = path.join(archived.planning, `spec-${slug}.md`);
|
|
333
416
|
const next = lib.toSlug(typeof opts.proxima === 'string' ? opts.proxima : '');
|
|
334
417
|
if (state.fromConfig === slug || !state.fromConfig) lib.writeConfigValue(state.root, 'active_feature', next);
|
|
335
418
|
if (next) {
|
|
336
419
|
const dirs = lib.artifactDirs(state.root);
|
|
337
420
|
for (const d of [dirs.planning, dirs.implementation]) fs.mkdirSync(path.join(d, next), { recursive: true });
|
|
338
421
|
}
|
|
339
|
-
const parts = [`
|
|
340
|
-
parts.push(
|
|
422
|
+
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.`);
|
|
424
|
+
parts.push(next ? `Sprint aberto agora: ${next}.` : 'Nenhum sprint aberto agora: o próximo pedido de trabalho abre um.');
|
|
341
425
|
if (state.fromBranch === slug) parts.push(`O branch ${state.git.branch} ainda aponta para ${slug}: volte ao tronco ou troque de branch.`);
|
|
342
426
|
const memo = opts.cerebro ? cerebroClose(state, slug, status, opts) : null;
|
|
343
427
|
if (memo && memo.gravado) parts.push(`Cérebro atualizado: ${memo.arquivo} (Estado + linha em Sessões).`);
|
|
@@ -346,31 +430,49 @@ function cmdEncerrar(state, opts) {
|
|
|
346
430
|
parts.push(`Registre no Cérebro: Estado do projeto + linha na tabela Sessões (${slug}: ${status} em ${lib.today()}).`);
|
|
347
431
|
}
|
|
348
432
|
if (pend.length > 0) parts.push(`Pendências: ${pend.map((p) => p.item).join('; ')}.`);
|
|
349
|
-
const data = {
|
|
433
|
+
const data = {
|
|
434
|
+
feature: slug,
|
|
435
|
+
status,
|
|
436
|
+
proxima: next,
|
|
437
|
+
pendencias: pend.map((p) => p.item),
|
|
438
|
+
arquivo: rel(state.root, archived.planning),
|
|
439
|
+
};
|
|
350
440
|
if (memo) data.cerebro = memo;
|
|
351
441
|
return { ok: true, data, line: parts.join(' ') };
|
|
352
442
|
}
|
|
353
443
|
|
|
354
|
-
function
|
|
355
|
-
const dirs = lib.artifactDirs(state.root);
|
|
356
|
-
let names = [];
|
|
444
|
+
function sprintDirs(base) {
|
|
357
445
|
try {
|
|
358
|
-
|
|
359
|
-
.readdirSync(
|
|
360
|
-
.filter((e) => e.isDirectory())
|
|
446
|
+
return fs
|
|
447
|
+
.readdirSync(base, { withFileTypes: true })
|
|
448
|
+
.filter((e) => e.isDirectory() && e.name !== lib.ARCHIVE_DIR)
|
|
361
449
|
.map((e) => e.name);
|
|
362
450
|
} catch {
|
|
363
|
-
|
|
451
|
+
return [];
|
|
364
452
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function cmdListar(state) {
|
|
456
|
+
const dirs = lib.artifactDirs(state.root);
|
|
457
|
+
const statusOf = (folder, slug) => {
|
|
458
|
+
const spec = path.join(folder, slug, `spec-${slug}.md`);
|
|
459
|
+
return fs.existsSync(spec) ? lib.parseFrontmatter(fs.readFileSync(spec, 'utf8')).data.status || '?' : 'sem spec';
|
|
460
|
+
};
|
|
461
|
+
const archive = path.join(dirs.planning, lib.ARCHIVE_DIR);
|
|
462
|
+
const features = [
|
|
463
|
+
...sprintDirs(dirs.planning).map((slug) => ({
|
|
464
|
+
feature: slug,
|
|
465
|
+
status: statusOf(dirs.planning, slug),
|
|
466
|
+
ativa: slug === state.slug,
|
|
467
|
+
arquivada: false,
|
|
468
|
+
})),
|
|
469
|
+
...sprintDirs(archive).map((slug) => ({ feature: slug, status: statusOf(archive, slug), ativa: false, arquivada: true })),
|
|
470
|
+
];
|
|
471
|
+
const abertas = features.filter((f) => !f.arquivada);
|
|
472
|
+
const arquivadas = features.length - abertas.length;
|
|
473
|
+
const lines = abertas.map((f) => `${f.ativa ? '*' : ' '} ${f.feature} (${f.status})`);
|
|
474
|
+
if (arquivadas > 0) lines.push(` + ${arquivadas} sprint(s) fechado(s) em ${rel(state.root, archive)}/`);
|
|
475
|
+
const line = features.length === 0 ? 'Nenhum sprint com pasta ainda.' : lines.join('\n');
|
|
374
476
|
return { ok: true, data: { features }, line };
|
|
375
477
|
}
|
|
376
478
|
|
|
@@ -161,9 +161,9 @@ function featureLine(wizzRoot) {
|
|
|
161
161
|
const state = lib.resolveFeature(wizzRoot);
|
|
162
162
|
const cmd = 'node _wizz/hooks/wizz-feature.js';
|
|
163
163
|
if (!state.slug) {
|
|
164
|
-
return ` Feature: nenhuma ativa. Pedido
|
|
164
|
+
return ` Feature: nenhuma ativa (nenhum sprint aberto). Pedido de trabalho → ANTES de despachar \`${cmd} definir <slug do pedido>\` (sem perguntar); pergunta/conversa não abre; handoff leva \`feature:\`.`;
|
|
165
165
|
}
|
|
166
|
-
return ` Feature: ${state.slug}.
|
|
166
|
+
return ` Feature: ${state.slug} (sprint aberto). Todo trabalho entra nele (assunto novo que cabe: ajuste pequeno só faz, vários passos vira story, objetivo novo vira épico); não cabe → pergunte 1 vez: adicionar ou abrir outro (\`definir <novo> --novo\` só com o dono); entrega fechada → \`${cmd} encerrar\`. Handoff leva \`feature: ${state.slug}\`.`;
|
|
167
167
|
} catch (error) {
|
|
168
168
|
debug('router-enforce', 'featureLine', error);
|
|
169
169
|
return '';
|
|
@@ -87,7 +87,10 @@ class Installer {
|
|
|
87
87
|
await prompts.log.warn(`Warning: Could not check for legacy WIZZ entries: ${error.message}`);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
this._runtimeConfigValues = undefined;
|
|
90
91
|
if (existingInstall.installed) {
|
|
92
|
+
// Antes de mexer em `_wizz/`: o sprint aberto e o layout que os hooks gravaram.
|
|
93
|
+
this._runtimeConfigValues = await moduleConfigWriter.captureRuntimeValues(realWizzDirCandidate);
|
|
91
94
|
await this._removeDeselectedModules(existingInstall, config, paths, originalConfig._preserveModules || []);
|
|
92
95
|
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
|
|
93
96
|
await this._removeDeselectedIdes(existingInstall, config, paths);
|
|
@@ -1412,7 +1415,10 @@ class Installer {
|
|
|
1412
1415
|
* @param {Object} moduleConfigs - Collected configuration values
|
|
1413
1416
|
*/
|
|
1414
1417
|
async generateModuleConfigs(wizzDir, moduleConfigs) {
|
|
1415
|
-
return moduleConfigWriter.generateModuleConfigs(wizzDir, moduleConfigs, {
|
|
1418
|
+
return moduleConfigWriter.generateModuleConfigs(wizzDir, moduleConfigs, {
|
|
1419
|
+
trackFile: (p) => this.installedFiles.add(p),
|
|
1420
|
+
runtimeValues: this._runtimeConfigValues,
|
|
1421
|
+
});
|
|
1416
1422
|
}
|
|
1417
1423
|
|
|
1418
1424
|
/**
|
|
@@ -10,14 +10,71 @@ const path = require('node:path');
|
|
|
10
10
|
const fs = require('../fs-native');
|
|
11
11
|
const { getProjectRoot } = require('../project-root');
|
|
12
12
|
|
|
13
|
+
// Chaves que os hooks gravam no config.yaml em tempo de uso, não o installer:
|
|
14
|
+
// `active_feature` (wizz-feature.js definir: o sprint aberto) e `layout`
|
|
15
|
+
// (wizz-arrumar.js layout --gravar). O installer lê as respostas do
|
|
16
|
+
// config.toml, onde elas não existem ou ficaram vazias, e regenerava o yaml do
|
|
17
|
+
// zero: o update da 2.20.0 zerou o sprint aberto e o layout de todo projeto.
|
|
18
|
+
const RUNTIME_KEYS = ['active_feature', 'layout'];
|
|
19
|
+
|
|
20
|
+
const isEmpty = (value) => value === undefined || value === null || value === '';
|
|
21
|
+
|
|
22
|
+
const NON_MODULE_DIRS = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom', 'hooks']);
|
|
23
|
+
|
|
24
|
+
/** Os valores de tempo de uso de um config.yaml (vazio se ausente ou ilegível). */
|
|
25
|
+
async function readRuntimeValues(configPath, yaml) {
|
|
26
|
+
let previous;
|
|
27
|
+
try {
|
|
28
|
+
previous = yaml.parse(await fs.readFile(configPath, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
if (!previous || typeof previous !== 'object' || Array.isArray(previous)) return {};
|
|
33
|
+
const kept = {};
|
|
34
|
+
for (const key of RUNTIME_KEYS) if (!isEmpty(previous[key])) kept[key] = previous[key];
|
|
35
|
+
return kept;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Lê os valores de tempo de uso de todo módulo instalado. O update chama ANTES
|
|
40
|
+
* de mexer em `_wizz/`: depois o yaml antigo já foi apagado junto com a pasta
|
|
41
|
+
* do módulo.
|
|
42
|
+
* @param {string} wizzDir
|
|
43
|
+
* @returns {Promise<Record<string, Record<string, unknown>>>} módulo → chave → valor
|
|
44
|
+
*/
|
|
45
|
+
async function captureRuntimeValues(wizzDir) {
|
|
46
|
+
const yaml = require('yaml');
|
|
47
|
+
const out = {};
|
|
48
|
+
let entries = [];
|
|
49
|
+
try {
|
|
50
|
+
entries = await fs.readdir(wizzDir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.isDirectory() || NON_MODULE_DIRS.has(entry.name)) continue;
|
|
56
|
+
const values = await readRuntimeValues(path.join(wizzDir, entry.name, 'config.yaml'), yaml);
|
|
57
|
+
if (Object.keys(values).length > 0) out[entry.name] = values;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resposta nova vence; o valor de tempo de uso só preenche o que ela deixaria vazio. */
|
|
63
|
+
function withRuntimeValues(answered, previous = {}) {
|
|
64
|
+
const config = { ...answered };
|
|
65
|
+
for (const [key, value] of Object.entries(previous)) if (isEmpty(config[key])) config[key] = value;
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
|
|
13
69
|
/**
|
|
14
70
|
* Generate clean config.yaml files for each installed module.
|
|
15
71
|
* @param {string} wizzDir - WIZZ installation directory
|
|
16
72
|
* @param {Object} moduleConfigs - Collected configuration values
|
|
17
73
|
* @param {Object} deps - Injected dependencies
|
|
18
74
|
* @param {(filePath: string) => void} deps.trackFile - Records a written file as installed
|
|
75
|
+
* @param {Record<string, Record<string, unknown>>} [deps.runtimeValues] - captureRuntimeValues() de antes do update
|
|
19
76
|
*/
|
|
20
|
-
async function generateModuleConfigs(wizzDir, moduleConfigs, { trackFile }) {
|
|
77
|
+
async function generateModuleConfigs(wizzDir, moduleConfigs, { trackFile, runtimeValues }) {
|
|
21
78
|
const yaml = require('yaml');
|
|
22
79
|
|
|
23
80
|
// Extract core config values to share with other modules
|
|
@@ -28,18 +85,18 @@ async function generateModuleConfigs(wizzDir, moduleConfigs, { trackFile }) {
|
|
|
28
85
|
// 'hooks' (RT-C1, tools/hooks/* copied here by Installer#_installHooks) is
|
|
29
86
|
// infra, not a module — without this exclusion this scanner treated it as
|
|
30
87
|
// one and wrote a spurious _wizz/hooks/config.yaml.
|
|
31
|
-
const
|
|
32
|
-
const installedModules = entries.filter((entry) => entry.isDirectory() && !nonModuleDirs.has(entry.name)).map((entry) => entry.name);
|
|
88
|
+
const installedModules = entries.filter((entry) => entry.isDirectory() && !NON_MODULE_DIRS.has(entry.name)).map((entry) => entry.name);
|
|
33
89
|
|
|
34
90
|
// Generate config.yaml for each installed module
|
|
35
91
|
for (const moduleName of installedModules) {
|
|
36
92
|
const modulePath = path.join(wizzDir, moduleName);
|
|
37
93
|
|
|
38
|
-
// Get module-specific config or use empty object if none
|
|
39
|
-
const config = moduleConfigs[moduleName] || {};
|
|
40
|
-
|
|
41
94
|
if (await fs.pathExists(modulePath)) {
|
|
42
95
|
const configPath = path.join(modulePath, 'config.yaml');
|
|
96
|
+
// Module-specific answers, plus what the hooks wrote since the last install
|
|
97
|
+
const answered = moduleConfigs[moduleName] || {};
|
|
98
|
+
const previous = runtimeValues?.[moduleName] ?? (await readRuntimeValues(configPath, yaml));
|
|
99
|
+
const config = withRuntimeValues(answered, previous);
|
|
43
100
|
|
|
44
101
|
// Create header
|
|
45
102
|
const packageJson = require(path.join(getProjectRoot(), 'package.json'));
|
|
@@ -110,4 +167,4 @@ async function generateModuleConfigs(wizzDir, moduleConfigs, { trackFile }) {
|
|
|
110
167
|
}
|
|
111
168
|
}
|
|
112
169
|
|
|
113
|
-
module.exports = { generateModuleConfigs };
|
|
170
|
+
module.exports = { generateModuleConfigs, captureRuntimeValues, RUNTIME_KEYS };
|