wendkeep 0.33.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.34.0] — 2026-07-11
8
+
9
+ ### Added
10
+
11
+ - **Spec efetiva por change**: `wendkeep spec effective --change <slug> [--json]` combina o
12
+ contrato consolidado com somente o delta selecionado, incluindo origem e operação por requisito.
13
+ - `change use <slug>` troca o ponteiro global; `change continue <arquivada> <novo-slug>` cria
14
+ continuação com backlink sem reabrir arquivo nem herdar evidência/verdict.
15
+ - `sync-defs --check`, metadata de versão/hash e entrega idêntica de skills em `.claude/skills`
16
+ e `.agents/skills`.
17
+ - `SPECS_STATE.json`, baseline por change e `spec migrate/rebase` para detectar edição direta e
18
+ conflitos concorrentes no mesmo requisito.
19
+
20
+ ### Changed
21
+
22
+ - `08-Mudanças/<slug>/specs/` é o único local de autoria. `07-Specs` permanece como contrato
23
+ consolidado gerado/read-only, preservando ADRs e links históricos.
24
+ - `verify --deep` agora grava requisitos completos em `verificacao.json` e sela o pacote com
25
+ `effectiveSpecHash`; `wk-verify` não relê `07-Specs`.
26
+ - `doctor` valida todas as changes abertas e avisa quando skills precisam de reseed + reinício.
27
+
28
+ ### Fixed
29
+
30
+ - Requisitos `ADDED` ainda não arquivados deixam de ficar invisíveis ao verificador independente.
31
+ - Archives concorrentes bloqueiam somente quando outra change alterou o mesmo requisito; mudanças
32
+ não relacionadas na mesma capability podem prosseguir.
33
+
34
+ ### Migration
35
+
36
+ - Rode `wendkeep spec migrate --vault <vault>` uma vez para adotar os contratos consolidados atuais.
37
+ - Rode `wendkeep sync-defs --reseed --vault <vault> --project .` e reinicie Claude Code/Codex.
38
+
7
39
  ## [0.33.0] — 2026-07-11
8
40
 
9
41
  ### Added
package/README.md CHANGED
@@ -71,7 +71,7 @@ npx wendkeep init
71
71
  Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
72
72
  5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
73
73
  6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
74
- 7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify` (some ship bundled templates, e.g. `wk-verify`'s `verdict-template.json` + reviewer prompt). `init` runs `wendkeep sync-defs` for you, delivering them to `.codex/agents/` + `.claude/skills/` (re‑run `sync-defs` after editing `.brain`).
74
+ 7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify`. `init` delivers them to `.codex/agents/`, `.claude/skills/`, and `.agents/skills/`; `sync-defs --check` detects stale copies.
75
75
  8. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` (a `validate-memory` sensor plus one per detected `package.json` script). Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
76
76
 
77
77
  ```bash
@@ -114,12 +114,12 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
114
114
  | `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
115
115
  | `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` (global backlog) / `show` / `status [slug]` / `done <id> [--change slug]` / `undone <id> [--change slug]` / `diff` / `archive [--force]`. |
116
116
  | `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
117
- | `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
117
+ | `wendkeep spec <sub>` | `list` / `show` generated contracts; `effective --change <slug>`; `migrate`; `rebase`. |
118
118
  | `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
119
119
  | `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
120
120
  | `wendkeep import [opts]` | **Retroactive memory** — backfill past **Claude + Codex** sessions into the vault (deduped by `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
121
121
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
122
- | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
122
+ | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
123
123
  | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
124
124
  | `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
125
125
  | `wendkeep --version` / `--help` | Version / usage. |
@@ -148,14 +148,14 @@ Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependenc
148
148
  explore → propose → apply (TDD) → verify → archive
149
149
  ```
150
150
 
151
- - **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). It becomes the global *current* change. Multiple changes may remain open: hooks and `change list/status` expose every pending task, while commands without `--change` use only the current one.
151
+ - **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). It becomes the global *current* change. Multiple changes may remain open; `change use <slug>` changes focus and `change continue <archived> <new>` creates an auditable continuation.
152
152
  - **Apply** — implement each `tarefas.md` task. Tag a task that needs machine proof with `[sensor:<id>]`.
153
- - **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
153
+ - **Verify** — `wendkeep verify` records sensor evidence. `verify --deep` builds a self-contained package with complete effective requirements (living contract + this change's delta), so the independent verifier never needs to reconstruct unarchived requirements from `07-Specs`.
154
154
  - **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
155
155
 
156
156
  > The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` is the human's call — never the agent's.**
157
157
 
158
- `wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) into `.brain/skills` and delivers them to `.claude/skills` the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
158
+ `wendkeep init` seeds process skills into `.brain/skills` and delivers identical copies to Claude Code and Codex. Every skill carries source hash/version metadata; `doctor` warns when reseed + agent restart is required.
159
159
 
160
160
  ### The loop in five minutes
161
161
 
@@ -170,13 +170,14 @@ Edit `tarefas.md` — tag proof and requirement per task:
170
170
  - [ ] 1.1 toggle persists across sessions [req:UI-1] [sensor:tests]
171
171
  ```
172
172
 
173
- Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
174
- `specs/ui/spec.md` (`## ADDED Requirements` `### Requisito: UI-1 — dark mode toggle`). Then:
173
+ Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta only in
174
+ `08-Mudanças/<slug>/specs/ui/spec.md`. `07-Specs` is generated/read-only. Then:
175
175
 
176
176
  ```bash
177
177
  npx wendkeep change status # one screen: tasks / sensors / verdict
178
178
  npx wendkeep change list # all open changes + pending tasks
179
179
  npx wendkeep change status dark-mode # one change in detail
180
+ npx wendkeep spec effective --change dark-mode # living contract + this change's delta
180
181
  npx wendkeep change done 1.1 # tick a task from the CLI
181
182
  npx wendkeep verify # run the declared sensors -> evidencia.json
182
183
  npx wendkeep verify --deep # assemble the verification package
@@ -185,7 +186,7 @@ npx wendkeep change diff # preview what will land in 07-Sp
185
186
  npx wendkeep change archive dark-mode # gate: sensors + verdict + no open tasks
186
187
  ```
187
188
 
188
- The archive promotes the delta into the living `07-Specs/ui.md`, mints an ADR, and the
189
+ The archive promotes the delta into generated `07-Specs/ui.md`, mints an ADR, and the
189
190
  Obsidian graph now links *session ↔ change ↔ requirement ↔ decision*. A change that names
190
191
  no `[req:]` skips the independent verdict — the sensor gate is its proof.
191
192
 
package/bin/wendkeep.mjs CHANGED
@@ -43,10 +43,10 @@ Usage:
43
43
  agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
44
44
 
45
45
  wendkeep doctor [--vault P] Run a vault health check.
46
- wendkeep change <sub> Change lifecycle: new [--simple] | list | show | status |
47
- done <id> | undone <id> | diff | archive [--force] | abandon [slug].
46
+ wendkeep change <sub> Change lifecycle: new [--simple] | use | continue | list | show |
47
+ status | done <id> | undone <id> | diff | archive [--force] | abandon.
48
48
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
49
- wendkeep spec <sub> Living specs: list | show <capability>.
49
+ wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
50
50
  wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
51
51
  wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
52
52
  --since <date> · --top [N] (priciest) · --trend [day|week|month]
@@ -70,9 +70,10 @@ Usage:
70
70
  --vault <path> or OBSIDIAN_VAULT_PATH if no path given.
71
71
  wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
72
72
  project: .brain/agents/*.toml -> .codex/agents,
73
- .brain/skills/<name> -> .claude/skills. --vault P --project P.
73
+ .brain/skills/<name> -> .claude/skills + .agents/skills. --vault P --project P.
74
74
  --reseed re-semeia as skills wk-* com os seeds da versão instalada
75
75
  (sobrescreve edições manuais nas wk-*) antes de copiar.
76
+ --check detecta drift sem modificar arquivos.
76
77
  wendkeep --version Print version.
77
78
  wendkeep --help Show this help.
78
79
  `;
@@ -4,7 +4,7 @@
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
7
- import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf } from './spec-core.mjs';
7
+ import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline } from './spec-core.mjs';
8
8
  import { getLocale } from './locale.mjs';
9
9
 
10
10
  export const ARCHIVE_DIR = '_arquivo';
@@ -119,10 +119,44 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
119
119
  writeFileSync(exampleDelta, files.specDelta, 'utf8');
120
120
  }
121
121
  }
122
+ if (!existed) captureSpecBaseline(vaultBase, dir);
122
123
  setActiveChange(vaultBase, slug);
123
124
  return { rel: changeDirRel(slug, vaultBase), created: !existed };
124
125
  }
125
126
 
127
+ export function useChange(vaultBase, slug) {
128
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
129
+ if (!existsSync(join(dir, 'proposta.md'))) return { ok: false, error: `change aberta não encontrada: ${slug}` };
130
+ setActiveChange(vaultBase, slug);
131
+ return { ok: true, rel: changeDirRel(slug, vaultBase) };
132
+ }
133
+
134
+ export function continueChange(vaultBase, archivedSlug, newSlug, options = {}) {
135
+ const loc = getLocale(vaultBase);
136
+ const archiveDir = join(vaultBase, loc.folders.changes, ARCHIVE_DIR);
137
+ let names = [];
138
+ try { names = readdirSync(archiveDir).filter((name) => statSync(join(archiveDir, name)).isDirectory()); } catch { /* no archive */ }
139
+ const matches = names.filter((name) => name === archivedSlug || name.endsWith(`-${archivedSlug}`));
140
+ if (!matches.length) return { ok: false, error: `change arquivada não encontrada: ${archivedSlug}` };
141
+ if (matches.length > 1) return { ok: false, error: `change arquivada ambígua: ${archivedSlug} (${matches.join(', ')})` };
142
+ if (existsSync(join(vaultBase, loc.folders.changes, newSlug, 'proposta.md'))) {
143
+ return { ok: false, error: `change de continuação já existe: ${newSlug}` };
144
+ }
145
+ const archivedName = matches[0];
146
+ const archivedProposal = join(loc.folders.changes, ARCHIVE_DIR, archivedName, 'proposta');
147
+ const result = newChange(vaultBase, newSlug, options);
148
+ const proposalPath = join(vaultBase, loc.folders.changes, newSlug, 'proposta.md');
149
+ let proposal = readFileSync(proposalPath, 'utf8');
150
+ proposal = proposal.replace(/^specs:/m, `continues: "${wikilinkFromRel(archivedProposal)}"\nspecs:`);
151
+ const heading = loc.id === 'en' ? '## Continuation' : '## Continuação';
152
+ const note = loc.id === 'en'
153
+ ? `Continues ${wikilinkFromRel(archivedProposal)}. Archived evidence and verdict are not inherited.`
154
+ : `Continua ${wikilinkFromRel(archivedProposal)}. Evidências e verdict da change arquivada não são herdados.`;
155
+ proposal = `${proposal.trimEnd()}\n\n${heading}\n\n${note}\n`;
156
+ writeFileSync(proposalPath, proposal, 'utf8');
157
+ return { ok: true, ...result, archived: archivedName };
158
+ }
159
+
126
160
  export function parseTasks(md) {
127
161
  const tasks = [];
128
162
  const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
@@ -219,7 +253,7 @@ export function renderOpenChanges(state, { tag = 'open_changes' } = {}) {
219
253
  else for (const task of change.openTasks) lines.push(`- [ ] ${task.id} ${task.text}`);
220
254
  }
221
255
  if (state.current) lines.push('Para change atual: `wendkeep change done <id>`; antes de archive: `wendkeep verify`.');
222
- lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change new <slug-existente>` ou use `--change <slug>` quando disponível.');
256
+ lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change use <slug>` ou use `--change <slug>` quando disponível.');
223
257
  if (tag) lines.push(`</${tag}>`);
224
258
  return lines.join('\n');
225
259
  }
@@ -3,13 +3,12 @@
3
3
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { activeChange, parseTasks } from './change-core.mjs';
6
- import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict, validateSpecImpact } from './spec-core.mjs';
6
+ import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tasksHashOf, validateSpecImpact } from './spec-core.mjs';
7
7
  import { getLocale } from './locale.mjs';
8
8
 
9
9
  export function checkHarness(vaultBase, projectRoot) {
10
10
  const loc = getLocale(vaultBase);
11
11
  const CHANGES_DIR = loc.folders.changes;
12
- const SPECS_DIR = loc.folders.specs;
13
12
  const errors = [];
14
13
  const warnings = [];
15
14
 
@@ -23,16 +22,9 @@ export function checkHarness(vaultBase, projectRoot) {
23
22
  } catch { errors.push('wendkeep.sensors.json: JSON inválido'); }
24
23
  }
25
24
 
26
- // Known requirement ids across the living specs (+ spec-without-source warning).
27
- const knownReqs = new Set();
28
- try {
29
- for (const f of readdirSync(join(vaultBase, SPECS_DIR))) {
30
- if (!f.endsWith('.md') || f === 'README.md') continue;
31
- const md = readFileSync(join(vaultBase, SPECS_DIR, f), 'utf8');
32
- for (const r of parseRequirements(md)) if (r.id) knownReqs.add(r.id);
33
- if (!/^>\s+Atualizado por/m.test(md)) warnings.push(`spec sem origem: ${SPECS_DIR}/${f}`);
34
- }
35
- } catch { /* sem 07-Specs */ }
25
+ const specState = checkSpecsState(vaultBase);
26
+ if (specState.missing) warnings.push('SPECS_STATE ausente — rode `wendkeep spec migrate`; 07-Specs deve ser gerado/read-only');
27
+ else if (!specState.ok) errors.push(`07-Specs alterado fora do WendKeep: ${specState.changed.join(', ')} — mova a alteração para 08-Mudanças/<change>/specs`);
36
28
 
37
29
  // 2/3. Changes: malformed dirs; the active change's deltas add to knownReqs.
38
30
  const active = activeChange(vaultBase);
@@ -46,15 +38,18 @@ export function checkHarness(vaultBase, projectRoot) {
46
38
  const impact = validateSpecImpact(dir);
47
39
  errors.push(...impact.errors.map((e) => `${name}: ${e}`));
48
40
  warnings.push(...impact.warnings.map((w) => `${name}: ${w}`));
49
- if (name === active) {
50
- try {
51
- for (const cap of parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8'))) {
52
- try {
53
- const d = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8'));
54
- for (const r of [...d.added, ...d.modified]) if (r.id) knownReqs.add(r.id);
55
- } catch { /* sem delta pra essa cap */ }
56
- }
57
- } catch { /* proposta ilegível */ }
41
+ let tasks = [];
42
+ let tarefasMd = '';
43
+ try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
44
+ const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
45
+ const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
46
+ errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
47
+ if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
48
+ let verdict = null;
49
+ try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
50
+ if (verdict && reqIds.length) {
51
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
52
+ if (!v.ok) warnings.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
58
53
  }
59
54
  }
60
55
 
@@ -63,17 +58,6 @@ export function checkHarness(vaultBase, projectRoot) {
63
58
  const dir = join(vaultBase, CHANGES_DIR, active);
64
59
  if (!existsSync(join(dir, 'proposta.md'))) {
65
60
  errors.push(`ponteiro CURRENT_CHANGE aponta pra change inexistente: ${active}`);
66
- } else {
67
- let tasks = [];
68
- try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
69
- const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
70
- for (const rid of reqIds) if (!knownReqs.has(rid)) errors.push(`req órfão em ${active}: [req:${rid}] não existe em 07-Specs nem no delta`);
71
- let verdict = null;
72
- try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
73
- if (verdict && reqIds.length) {
74
- const v = evaluateVerdict(verdict, reqIds);
75
- if (!v.ok) warnings.push(`verdict stale em ${active}: falta cobrir ${v.missing.join(', ')}`);
76
- }
77
61
  }
78
62
  }
79
63
 
@@ -11,6 +11,14 @@ export function tasksHashOf(md) {
11
11
  return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
12
12
  }
13
13
 
14
+ export function contentHashOf(value) {
15
+ return createHash('sha256').update(String(value)).digest('hex');
16
+ }
17
+
18
+ export const SPECS_STATE_FILE = '.brain/SPECS_STATE.json';
19
+ export const SPEC_BASELINE_FILE = '.spec-base.json';
20
+ export const MANAGED_SPEC_MARKER = '<!-- wendkeep:managed-spec — generated from 08-Mudanças; do not edit directly -->';
21
+
14
22
  // Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
15
23
  const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
16
24
 
@@ -21,7 +29,7 @@ export function parseRequirements(md) {
21
29
  for (let i = 0; i < matches.length; i += 1) {
22
30
  const raw = matches[i][1].trim();
23
31
  // Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"; else the whole text.
24
- const idM = raw.match(/^([A-Z][A-Z0-9]*-\d+)\s*—\s*(.+)$/);
32
+ const idM = raw.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+)\s*—\s*(.+)$/);
25
33
  const id = idM ? idM[1] : null;
26
34
  const name = idM ? idM[2].trim() : raw;
27
35
  const start = matches[i].index + matches[i][0].length;
@@ -75,7 +83,148 @@ export function applyDelta(reqs, delta) {
75
83
  export function renderSpec(capability, reqs, { footer, reqHeading = 'Requisito' } = {}) {
76
84
  const blocks = reqs.map((r) => `### ${reqHeading}: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
77
85
  const foot = footer ? `\n\n> ${footer}\n` : '\n';
78
- return `---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
86
+ return `${MANAGED_SPEC_MARKER}\n---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
87
+ }
88
+
89
+ function readLivingSpecs(vaultBase) {
90
+ const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
91
+ const specs = {};
92
+ let files = [];
93
+ try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { return specs; }
94
+ for (const file of files) {
95
+ const capability = file.replace(/\.md$/, '');
96
+ const md = readFileSync(join(specsDir, file), 'utf8');
97
+ specs[capability] = {
98
+ hash: contentHashOf(md),
99
+ requirements: Object.fromEntries(parseRequirements(md).map((r) => [r.id || r.name, contentHashOf(JSON.stringify(r))])),
100
+ };
101
+ }
102
+ return specs;
103
+ }
104
+
105
+ export function livingSpecCapabilities(vaultBase) {
106
+ return Object.keys(readLivingSpecs(vaultBase));
107
+ }
108
+
109
+ export function adoptSpecsState(vaultBase) {
110
+ const state = { version: 1, generatedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
111
+ ensureDir(join(vaultBase, '.brain'));
112
+ writeFileSync(join(vaultBase, SPECS_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
113
+ return state;
114
+ }
115
+
116
+ export function readSpecsState(vaultBase) {
117
+ try { return JSON.parse(readFileSync(join(vaultBase, SPECS_STATE_FILE), 'utf8')); } catch { return null; }
118
+ }
119
+
120
+ export function checkSpecsState(vaultBase) {
121
+ const recorded = readSpecsState(vaultBase);
122
+ if (!recorded) return { ok: false, missing: true, changed: [] };
123
+ const current = readLivingSpecs(vaultBase);
124
+ const names = new Set([...Object.keys(recorded.specs || {}), ...Object.keys(current)]);
125
+ const changed = [...names].filter((name) => recorded.specs?.[name]?.hash !== current[name]?.hash);
126
+ return { ok: changed.length === 0, missing: false, changed, current, recorded };
127
+ }
128
+
129
+ function recordPromotedSpecs(vaultBase, capabilities) {
130
+ const existing = readSpecsState(vaultBase);
131
+ if (!existing) return adoptSpecsState(vaultBase);
132
+ const current = readLivingSpecs(vaultBase);
133
+ const specs = { ...(existing.specs || {}) };
134
+ for (const capability of capabilities) {
135
+ if (current[capability]) specs[capability] = current[capability];
136
+ else delete specs[capability];
137
+ }
138
+ const state = { version: 1, generatedAt: new Date().toISOString(), specs };
139
+ writeFileSync(join(vaultBase, SPECS_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
140
+ return state;
141
+ }
142
+
143
+ export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } = {}) {
144
+ const path = join(changeDir, SPEC_BASELINE_FILE);
145
+ if (!refresh && existsSync(path)) {
146
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
147
+ }
148
+ const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
149
+ writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
150
+ return baseline;
151
+ }
152
+
153
+ export function specConflicts(vaultBase, changeDir, capabilities = discoverSpecDeltas(changeDir)) {
154
+ let baseline = null;
155
+ try { baseline = JSON.parse(readFileSync(join(changeDir, SPEC_BASELINE_FILE), 'utf8')); } catch { /* legacy change */ }
156
+ if (!baseline) return [];
157
+ const current = readLivingSpecs(vaultBase);
158
+ const conflicts = [];
159
+ for (const capability of capabilities) {
160
+ let delta;
161
+ try { delta = parseDelta(readFileSync(join(changeDir, 'specs', capability, 'spec.md'), 'utf8')); } catch { continue; }
162
+ const baseReqs = baseline.specs?.[capability]?.requirements || {};
163
+ const currentReqs = current[capability]?.requirements || {};
164
+ for (const req of delta.added || []) {
165
+ const key = req.id || req.name;
166
+ if (!(key in baseReqs) && key in currentReqs) conflicts.push(`${capability}:${key} foi adicionado por outra change`);
167
+ }
168
+ for (const req of delta.modified || []) {
169
+ const key = req.id || req.name;
170
+ if (baseReqs[key] !== currentReqs[key]) conflicts.push(`${capability}:${key} mudou desde a abertura da change`);
171
+ }
172
+ for (const key of delta.removed || []) {
173
+ if (baseReqs[key] !== currentReqs[key]) conflicts.push(`${capability}:${key} mudou desde a abertura da change`);
174
+ }
175
+ }
176
+ return conflicts;
177
+ }
178
+
179
+ export function resolveEffectiveSpecs(vaultBase, changeDir, capabilities = discoverSpecDeltas(changeDir)) {
180
+ const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
181
+ const result = [];
182
+ const warnings = [];
183
+ const errors = [];
184
+ for (const capability of capabilities) {
185
+ let living = [];
186
+ try { living = parseRequirements(readFileSync(join(specsDir, `${capability}.md`), 'utf8')); } catch { /* new capability */ }
187
+ let deltaMd = '';
188
+ try { deltaMd = readFileSync(join(changeDir, 'specs', capability, 'spec.md'), 'utf8'); } catch { /* unchanged living capability */ }
189
+ if (deltaMd && isPlaceholderDelta(deltaMd)) deltaMd = '';
190
+ const delta = deltaMd ? parseDelta(deltaMd) : { added: [], modified: [], removed: [] };
191
+ const operations = new Map(living.map((r) => [r.id || r.name, { operation: 'BASE', source: 'living' }]));
192
+ for (const r of delta.added) operations.set(r.id || r.name, { operation: 'ADDED', source: 'change' });
193
+ for (const r of delta.modified) operations.set(r.id || r.name, { operation: 'MODIFIED', source: 'change' });
194
+ for (const key of delta.removed) operations.delete(key);
195
+ const applied = applyDelta(living, delta);
196
+ warnings.push(...applied.warnings.map((w) => `${capability}: ${w}`));
197
+ for (const warning of applied.warnings) {
198
+ if (/^(ADDED já existe|MODIFIED inexistente|REMOVED inexistente)/.test(warning)) errors.push(`${capability}: ${warning}`);
199
+ }
200
+ result.push({
201
+ capability,
202
+ requirements: applied.reqs.map((r) => ({ ...r, capability, ...(operations.get(r.id || r.name) || { operation: 'BASE', source: 'living' }) })),
203
+ });
204
+ }
205
+ const serializable = result.map((s) => ({ capability: s.capability, requirements: s.requirements }));
206
+ return { specs: result, requirements: result.flatMap((s) => s.requirements), warnings, errors, hash: contentHashOf(JSON.stringify(serializable)) };
207
+ }
208
+
209
+ export function buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds = []) {
210
+ let listed = [];
211
+ try { listed = parseSpecsList(readFileSync(join(changeDir, 'proposta.md'), 'utf8')); } catch { /* caller validates change */ }
212
+ const changed = [...new Set([...listed, ...discoverSpecDeltas(changeDir)])];
213
+ const capabilities = [...new Set([...livingSpecCapabilities(vaultBase), ...changed])];
214
+ const effective = resolveEffectiveSpecs(vaultBase, changeDir, capabilities);
215
+ const byId = new Map(effective.requirements.filter((r) => r.id).map((r) => [r.id, r]));
216
+ const missing = reqIds.filter((id) => !byId.has(id));
217
+ const requirements = reqIds.map((id) => byId.get(id)).filter(Boolean);
218
+ const relevantCaps = new Set([...changed, ...requirements.map((r) => r.capability)]);
219
+ const relevant = effective.specs.filter((spec) => relevantCaps.has(spec.capability));
220
+ return {
221
+ ...effective,
222
+ specs: relevant,
223
+ requirements,
224
+ missing,
225
+ changedCapabilities: changed,
226
+ hash: contentHashOf(JSON.stringify(relevant)),
227
+ };
79
228
  }
80
229
 
81
230
  export function parseSpecsList(propostaMd) {
@@ -160,6 +309,13 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
160
309
  const specsDir = loc.folders.specs;
161
310
  const promoted = [];
162
311
  const warnings = [];
312
+ const state = checkSpecsState(vaultBase);
313
+ const unmanaged = state.missing ? [] : state.changed.filter((capability) => specs.includes(capability));
314
+ if (unmanaged.length) {
315
+ throw new Error(`07-Specs alterado fora do WendKeep: ${unmanaged.join(', ')} — mova o delta para 08-Mudanças/<change>/specs`);
316
+ }
317
+ const conflicts = specConflicts(vaultBase, changeDir, specs);
318
+ if (conflicts.length) throw new Error(`conflito de spec: ${conflicts.join('; ')} — reconcilie o delta e rode \`wendkeep spec rebase --change <slug> --accept-current\``);
163
319
  for (const cap of specs) {
164
320
  let deltaMd;
165
321
  try { deltaMd = readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8'); }
@@ -176,13 +332,14 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
176
332
  writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer, reqHeading: loc.reqHeading }), 'utf8');
177
333
  promoted.push(cap);
178
334
  }
335
+ recordPromotedSpecs(vaultBase, promoted);
179
336
  return { promoted, warnings };
180
337
  }
181
338
 
182
339
  // Gate check for the independent verdict (Wave A). A requirement-bearing change must have
183
340
  // a verdict that is ok and covers every declared req id. A requirement-less change passes:
184
341
  // nothing for an independent verifier to check — the sensor gate is already the proof.
185
- export function evaluateVerdict(verdict, reqIds, { tasksHash } = {}) {
342
+ export function evaluateVerdict(verdict, reqIds, { tasksHash, effectiveSpecHash } = {}) {
186
343
  const ids = reqIds || [];
187
344
  if (ids.length === 0) return { ok: true, missing: [] };
188
345
  if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
@@ -191,6 +348,9 @@ export function evaluateVerdict(verdict, reqIds, { tasksHash } = {}) {
191
348
  if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
192
349
  return { ok: false, missing: [], stale: true };
193
350
  }
351
+ if (effectiveSpecHash && verdict.effectiveSpecHash && verdict.effectiveSpecHash !== effectiveSpecHash) {
352
+ return { ok: false, missing: [], stale: true };
353
+ }
194
354
  const covered = new Set((verdict.coverage || []).filter((c) => c.covered).map((c) => c.req));
195
355
  const missing = ids.filter((r) => !covered.has(r));
196
356
  return { ok: missing.length === 0, missing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/change.mjs CHANGED
@@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import {
5
5
  newChange,
6
+ useChange,
7
+ continueChange,
6
8
  activeChange,
7
9
  allChangesState,
8
10
  listChanges,
@@ -14,7 +16,7 @@ import {
14
16
  scaffoldPlaceholders,
15
17
  } from '../hooks/change-core.mjs';
16
18
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
17
- import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
19
+ import { buildEffectiveRequirementPackage, evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
18
20
  import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
19
21
  import { getLocale } from '../hooks/locale.mjs';
20
22
 
@@ -62,6 +64,32 @@ export function runChange(argv) {
62
64
  process.exit(0);
63
65
  }
64
66
 
67
+ if (sub === 'use') {
68
+ const slug = slugArg();
69
+ if (!slug) { process.stderr.write('wendkeep change use: missing <slug>\n'); process.exit(2); }
70
+ const r = useChange(vaultBase, slug);
71
+ if (!r.ok) { process.stderr.write(`wendkeep change use: ${r.error}\n`); process.exit(2); }
72
+ process.stdout.write(`current change: ${slug}\n`);
73
+ process.exit(0);
74
+ }
75
+
76
+ if (sub === 'continue') {
77
+ const positionals = rest.filter((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
78
+ const [archivedSlug, newSlug] = positionals;
79
+ if (!archivedSlug || !newSlug) {
80
+ process.stderr.write('wendkeep change continue: use <archived-slug> <new-slug>\n');
81
+ process.exit(2);
82
+ }
83
+ let sessionRel = '';
84
+ try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* no control */ }
85
+ const r = continueChange(vaultBase, archivedSlug, newSlug, {
86
+ dateStr: today(), simple: rest.includes('--simple'), sessionRel,
87
+ });
88
+ if (!r.ok) { process.stderr.write(`wendkeep change continue: ${r.error}\n`); process.exit(2); }
89
+ process.stdout.write(`change created: ${r.rel} (continues ${r.archived}; active)\n`);
90
+ process.exit(0);
91
+ }
92
+
65
93
  if (sub === 'list') {
66
94
  const state = allChangesState(vaultBase);
67
95
  const { archived } = listChanges(vaultBase);
@@ -113,12 +141,16 @@ export function runChange(argv) {
113
141
  if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
114
142
  else process.stdout.write('evidencia: ausente\n');
115
143
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
144
+ const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
145
+ if (effective.errors.length || effective.missing.length) {
146
+ process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
147
+ }
116
148
  let verdict = null;
117
149
  try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
118
150
  if (!verdict) process.stdout.write(`verdict: ausente — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ' (verdict trivial automático)'}\n`);
119
151
  else if (!reqIds.length) process.stdout.write(`verdict: ${verdict.ok === true ? 'ok (trivial)' : 'não-ok — re-verifique'}\n`);
120
152
  else {
121
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
153
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
122
154
  process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
123
155
  }
124
156
  try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
@@ -195,6 +227,9 @@ export function runChange(argv) {
195
227
  }
196
228
  }
197
229
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
230
+ const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
231
+ if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
232
+ if (effective.missing.length) return { ok: false, failing: [`requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}`] };
198
233
  let evidence = [];
199
234
  try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
200
235
  const s = evaluateGate(evidence, required);
@@ -214,8 +249,16 @@ export function runChange(argv) {
214
249
  if (verdict.tasksHash && verdict.tasksHash !== hash) {
215
250
  return { ok: false, failing: [`verdict stale (tarefas.md mudou depois da verificação) — re-verifique: \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
216
251
  }
252
+ let verification = null;
253
+ try { verification = JSON.parse(readFileSync(join(dir, 'verificacao.json'), 'utf8')); } catch { /* none */ }
254
+ if (verification?.effectiveSpecHash && verification.effectiveSpecHash !== effective.hash) {
255
+ return { ok: false, failing: ['pacote de verificação stale (spec efetiva mudou) — rode `wendkeep verify --deep` novamente'] };
256
+ }
257
+ if (reqIds.length && verification?.effectiveSpecHash && !verdict.effectiveSpecHash) {
258
+ return { ok: false, failing: ['verdict sem effectiveSpecHash — rode a skill wk-verify novamente'] };
259
+ }
217
260
  if (reqIds.length) {
218
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: hash });
261
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: hash, effectiveSpecHash: effective.hash });
219
262
  if (!v.ok) {
220
263
  if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
221
264
  return { ok: false, failing: [`verdict incompleto: falta ${v.missing.join(', ')}`] };
@@ -250,6 +293,6 @@ export function runChange(argv) {
250
293
  process.exit(0);
251
294
  }
252
295
 
253
- process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, status, done, undone, diff, archive, abandon.\n`);
296
+ process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, use, continue, list, show, status, done, undone, diff, archive, abandon.\n`);
254
297
  process.exit(2);
255
298
  }
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { existsSync } from 'node:fs';
5
5
  import { dirname, isAbsolute, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { checkHarness } from '../hooks/harness-doctor.mjs';
8
+ import { checkSyncDefs } from './sync-defs.mjs';
8
9
 
9
10
  export function runDoctor(argv) {
10
11
  const here = dirname(fileURLToPath(import.meta.url));
@@ -40,6 +41,11 @@ export function runDoctor(argv) {
40
41
 
41
42
  // 2. Harness integrity (Wave B).
42
43
  const { errors, warnings } = checkHarness(vaultBase, projectRoot);
44
+ const defs = checkSyncDefs(vaultBase, projectRoot);
45
+ if (!defs.ok) {
46
+ warnings.push(...defs.issues.map((issue) => `defs: ${issue}`));
47
+ warnings.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
48
+ }
43
49
  process.stdout.write(`\n[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\n`);
44
50
  for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
45
51
  for (const w of warnings) process.stdout.write(` ! ${w}\n`);
package/src/init.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // OBSIDIAN_VAULT_PATH into .claude/settings.json, and adds the mcpvault server to
4
4
  // .mcp.json. Idempotent: re-running only adds what is missing.
5
5
  import { spawnSync } from 'node:child_process';
6
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
7
7
  import { basename, isAbsolute, join, resolve } from 'node:path';
8
8
  import { createInterface } from 'node:readline/promises';
9
9
  import {
@@ -40,6 +40,7 @@ import { seedDefinitions, syncDefs } from './sync-defs.mjs';
40
40
  import { seedWkSkills } from './skills-seed.mjs';
41
41
  import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
42
42
  import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
43
+ import { adoptSpecsState, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
43
44
 
44
45
  function parseArgs(argv) {
45
46
  const args = { mcp: true, yes: false, force: false };
@@ -278,7 +279,7 @@ const MESSAGES = {
278
279
  colorsOn: 'wendkeep-colors (snippet + grupos do grafo)',
279
280
  taxonomy: (n, c, loc, readme, views) => ` [1/4] taxonomia do vault: ${n} pastas (${c} criadas, locale ${loc})${readme}, .brain + change/spec + sensores semeados${views}`,
280
281
  readmeCreated: ', README.md criado', viewsNote: (n) => `, ${n} view(s) + dashboard`,
281
- defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
282
+ defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
282
283
  settingsBadJson: (p) => ` [2/4] settings.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
283
284
  settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wirados, OBSIDIAN_VAULT_PATH setado${bak})`,
284
285
  mcpBadJson: (p) => ` [3/4] .mcp.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
@@ -303,7 +304,7 @@ const MESSAGES = {
303
304
  colorsOn: 'wendkeep-colors (snippet + graph groups)',
304
305
  taxonomy: (n, c, loc, readme, views) => ` [1/4] vault taxonomy: ${n} folders (${c} created, locale ${loc})${readme}, .brain + change/spec + sensors seeded${views}`,
305
306
  readmeCreated: ', README.md created', viewsNote: (n) => `, ${n} view(s) + dashboard`,
306
- defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
307
+ defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
307
308
  settingsBadJson: (p) => ` [2/4] settings.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
308
309
  settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${bak})`,
309
310
  mcpBadJson: (p) => ` [3/4] .mcp.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
@@ -482,8 +483,12 @@ export async function runInit(argv) {
482
483
  const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
483
484
  if (!existsSync(specsReadme)) {
484
485
  writeFileSync(specsReadme, en
485
- ? `# Specs — living contract\n\nThe project's capabilities (requirements/scenarios). Changes in \`${loc.folders.changes}/\` promote deltas here on \`wendkeep change archive\`.\n`
486
- : `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
486
+ ? `# Specs — generated living contract\n\nRead-only: do not author here. Write deltas only in \`${loc.folders.changes}/<slug>/specs/\`; archive promotes them here.\n`
487
+ : `# Specs — contrato consolidado gerado\n\nSomente leitura: não edite aqui. Escreva deltas apenas em \`${loc.folders.changes}/<slug>/specs/\`; o archive promove para cá.\n`, 'utf8');
488
+ }
489
+ if (!existsSync(join(vaultPath, SPECS_STATE_FILE))) {
490
+ const livingSpecs = readdirSync(join(vaultPath, loc.folders.specs)).filter((name) => name.endsWith('.md') && name !== 'README.md');
491
+ if (!livingSpecs.length) adoptSpecsState(vaultPath);
487
492
  }
488
493
  const changeTpl = join(vaultPath, 'Templates', 'Change.md');
489
494
  if (!existsSync(changeTpl)) {
@@ -1,6 +1,6 @@
1
1
  // src/skills-seed.mjs — native, zero-dep process skills (Pilar A: the HOW layer).
2
2
  // Seeded into the vault's .brain/skills; distributed by `wendkeep sync-defs` to
3
- // .claude/skills. Wendkeep-flavored (reference change/verify), concise native prose.
3
+ // .claude/skills + .agents/skills. Wendkeep-flavored, concise native prose.
4
4
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
@@ -41,12 +41,13 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
41
41
  (teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
42
42
  - \`[sensor:<id>]\` — a prova automatizada (roda no verify).
43
43
  - \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
44
- quando a change mexe numa capability de \`07-Specs\`.
44
+ quando a change mexe numa capability. Toda autoria de spec ocorre somente em
45
+ \`08-Mudanças/<slug>/specs/<capability>/spec.md\`; \`07-Specs\` é gerado/read-only.
45
46
  Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
46
47
  4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
47
48
  \`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
48
49
  5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
49
- re-deriva a cobertura do \`07-Specs\` e grava \`verdict.json\`. Change trivial (sem
50
+ usa somente os requisitos autocontidos de \`verificacao.json\` e grava \`verdict.json\`. Change trivial (sem
50
51
  \`[req:]\`) recebe verdict automático — pula este passe.
51
52
  6. **Archive** — \`wendkeep change archive <slug>\`. O *gate* exige sensores verdes **E**
52
53
  \`verdict.json\` cobrindo os \`[req:]\`. Passando, promove os deltas pro \`07-Specs\`,
@@ -55,8 +56,8 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
55
56
  ## Regras
56
57
 
57
58
  - Várias changes podem ficar abertas. \`CURRENT_CHANGE.md\` marca uma atual, sem esconder as
58
- outras pendências. Claude, Codex ou outro agente podem assumir uma change existente com
59
- \`wendkeep change new <slug-existente>\` ou \`--change <slug>\` quando disponível.
59
+ outras pendências. Claude, Codex ou outro agente assumem uma change existente com
60
+ \`wendkeep change use <slug>\` ou \`--change <slug>\` quando disponível.
60
61
  - Se uma tarefa não precisa de prova automatizada, não declare sensor — o gate só exige
61
62
  o que você declarou. Sem \`[sensor:]\`, o archive não trava.
62
63
  - A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
@@ -77,7 +78,8 @@ vermelho pedindo por ele — e nunca escreva um teste que passaria sob a impleme
77
78
 
78
79
  ## Derive do spec, não do código
79
80
 
80
- Escreva a asserção a partir do *critério de aceite* do requisito (\`07-Specs\`), não lendo a
81
+ Escreva a asserção a partir do *critério de aceite* da spec efetiva
82
+ (\`wendkeep spec effective --change <slug>\`), não lendo a
81
83
  implementação. Cada asserção mira o resultado que o spec definiu. Escrever o teste lendo o
82
84
  código = ele só confirma o que o código já faz, bugs inclusos.
83
85
 
@@ -214,13 +216,13 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
214
216
  ## O que fazer
215
217
 
216
218
  1. Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência).
217
- 2. **Re-derive a cobertura do \`07-Specs\`** — pra cada \`[req:ID]\` da change, cheque se o
218
- comportamento definido no requisito está coberto por um teste que discrimina (não
219
+ 2. **Re-derive a cobertura do pacote autocontido** — pra cada requisito completo em
220
+ \`verificacao.json\`, cheque se o comportamento está coberto por um teste que discrimina (não
219
221
  passaria sob impl errada). Evidência \`arquivo:linha\`.
220
222
  3. Outcome check ancorado no spec: o resultado observável bate com o critério de aceite?
221
223
  4. Grave \`08-Mudanças/<slug>/verdict.json\`:
222
- \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "tasksHash": "<copie do verificacao.json>", "notes": [] }\`.
223
- O \`tasksHash\` vem do pacote — é o selo de frescor; sem ele (ou com tarefas alteradas
224
+ \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "tasksHash": "<copie do verificacao.json>", "effectiveSpecHash": "<copie do verificacao.json>", "notes": [] }\`.
225
+ \`tasksHash\` e \`effectiveSpecHash\` vêm do pacote — são selos de frescor; sem eles (ou com tarefas/spec alteradas
224
226
  depois), o gate rejeita o verdict como stale.
225
227
 
226
228
  ## Regras
@@ -259,6 +261,7 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
259
261
  needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
260
262
  3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
261
263
  \`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
264
+ Author specs only in \`08-Changes/<slug>/specs/\`; \`07-Specs\` is generated/read-only.
262
265
  4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
263
266
  the verification package.
264
267
  5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
@@ -269,7 +272,7 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
269
272
  ## Rules
270
273
  - Multiple changes may stay open. \`CURRENT_CHANGE.md\` marks one current change without hiding
271
274
  other pending tasks. Any agent may take over an existing change with
272
- \`wendkeep change new <existing-slug>\` or \`--change <slug>\` where available.
275
+ \`wendkeep change use <slug>\` or \`--change <slug>\` where available.
273
276
  - No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
274
277
  - The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
275
278
  `;
@@ -286,7 +289,7 @@ would pass under the wrong implementation.
286
289
  4. **Refactor** with the greens protecting you.
287
290
 
288
291
  ## Derive from the spec, not the code
289
- Write assertions from the requirement's acceptance criteria (\`07-Specs\`), not by reading the
292
+ Write assertions from the effective requirement (\`wendkeep spec effective --change <slug>\`), not by reading the
290
293
  implementation. Reading the code to write the test = it only confirms what the code already does.
291
294
 
292
295
  ## Non-shallow litmus
@@ -375,17 +378,18 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
375
378
 
376
379
  ## What to do
377
380
  1. Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence).
378
- 2. **Re-derive coverage from \`07-Specs\`** — for each \`[req:ID]\`, check the requirement's behaviour
381
+ 2. **Re-derive coverage from the self-contained package** — for each complete requirement in
382
+ \`verificacao.json\`, check its behaviour
379
383
  is covered by a test that discriminates (wouldn't pass under a wrong impl). \`file:line\` evidence.
380
384
  3. Spec-anchored outcome check: does the observable result match the acceptance criterion?
381
385
  4. Write \`08-Changes/<slug>/verdict.json\`:
382
- \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "notes": [] }\`.
386
+ \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "effectiveSpecHash": "<copy from the package>", "notes": [] }\`.
383
387
 
384
388
  ## Rules
385
389
  - **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
386
390
  - \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
387
391
  - Don't fix here — a gap becomes a fix task; re-verify after.
388
- - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
392
+ - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\` and \`effectiveSpecHash\`) covering every \`[req:]\`.
389
393
 
390
394
  ## Templates (in this folder)
391
395
  - \`spec-reviewer-prompt.md\` — hand it to the verifier sub-agent you spawn (read-only, author≠verifier).
@@ -402,6 +406,7 @@ const VERDICT_TEMPLATE = `{
402
406
  { "req": "GATE-1", "covered": true, "evidence": "tests/foo.test.mjs:42" }
403
407
  ],
404
408
  "tasksHash": "<copie de verificacao.json — selo de frescor / copy from verificacao.json — freshness seal>",
409
+ "effectiveSpecHash": "<copie de verificacao.json / copy from verificacao.json>",
405
410
  "notes": []
406
411
  }
407
412
  `;
@@ -415,8 +420,8 @@ Claude). Ele NÃO é o autor: entra fresco, read-only, não edita nada.
415
420
  Você é o verificador independente de uma mudança do wendkeep. Não escreveu este código —
416
421
  entre como se nunca o tivesse visto. Read-only.
417
422
 
418
- Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência) e os
419
- specs vivos em \`07-Specs/<capability>.md\`.
423
+ Leia somente o pacote autocontido \`08-Mudanças/<slug>/verificacao.json\` (requisitos completos,
424
+ tarefas e evidência). Não reabra \`07-Specs\`: ele ainda não contém deltas não arquivados.
420
425
 
421
426
  Para cada \`[req:ID]\` da mudança:
422
427
  1. Leia o **critério de aceite do requisito** no spec — NÃO leia a implementação primeiro.
@@ -427,7 +432,7 @@ Para cada \`[req:ID]\` da mudança:
427
432
 
428
433
  Grave \`08-Mudanças/<slug>/verdict.json\` no formato de \`verdict-template.json\`. \`ok: false\` se
429
434
  qualquer \`[req:]\` não tem cobertura que discrimina. Não conserte aqui — gap vira tarefa de
430
- correção. O \`tasksHash\` vem do pacote (selo de frescor; alterou tarefa depois, o gate rejeita).
435
+ correção. \`tasksHash\` e \`effectiveSpecHash\` vêm do pacote; alterações posteriores deixam o verdict stale.
431
436
  ---
432
437
  `;
433
438
 
@@ -440,8 +445,8 @@ It is NOT the author: fresh context, read-only, edits nothing.
440
445
  You are the independent verifier of a wendkeep change. You did not write this code — enter as if
441
446
  you'd never seen it. Read-only.
442
447
 
443
- Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence) and the
444
- living specs in \`07-Specs/<capability>.md\`.
448
+ Read only the self-contained \`08-Changes/<slug>/verificacao.json\` package (complete requirements,
449
+ tasks, evidence). Do not reopen \`07-Specs\`: it does not contain unarchived deltas yet.
445
450
 
446
451
  For each \`[req:ID]\`:
447
452
  1. Read the requirement's **acceptance criterion** in the spec — do NOT read the implementation first.
@@ -452,7 +457,7 @@ For each \`[req:ID]\`:
452
457
 
453
458
  Write \`08-Changes/<slug>/verdict.json\` in the shape of \`verdict-template.json\`. \`ok: false\` if any
454
459
  \`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`
455
- comes from the package (freshness seal; edit a task later and the gate rejects it as stale).
460
+ and \`effectiveSpecHash\` come from the package (freshness seals; later task/spec edits make verdict stale).
456
461
  ---
457
462
  `;
458
463
 
package/src/spec.mjs CHANGED
@@ -1,7 +1,15 @@
1
1
  // `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
2
2
  import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
- import { parseRequirements } from '../hooks/spec-core.mjs';
4
+ import {
5
+ adoptSpecsState,
6
+ buildEffectiveRequirementPackage,
7
+ captureSpecBaseline,
8
+ discoverSpecDeltas,
9
+ parseRequirements,
10
+ specConflicts,
11
+ } from '../hooks/spec-core.mjs';
12
+ import { activeChange, parseTasks } from '../hooks/change-core.mjs';
5
13
  import { getLocale } from '../hooks/locale.mjs';
6
14
 
7
15
  function resolveVault(argv) {
@@ -24,6 +32,64 @@ export function runSpec(argv) {
24
32
  const vaultBase = resolveVault(rest);
25
33
  const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
26
34
 
35
+ const option = (name) => {
36
+ const index = rest.indexOf(name);
37
+ if (index >= 0) return rest[index + 1];
38
+ const entry = rest.find((a) => a.startsWith(`${name}=`));
39
+ return entry ? entry.slice(name.length + 1) : undefined;
40
+ };
41
+
42
+ if (sub === 'effective') {
43
+ const slug = option('--change') || activeChange(vaultBase);
44
+ if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
45
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
46
+ let tasks = [];
47
+ try {
48
+ readFileSync(join(changeDir, 'proposta.md'), 'utf8');
49
+ tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
50
+ }
51
+ catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
52
+ const reqIds = [...new Set(tasks.map((task) => task.req).filter(Boolean))];
53
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
54
+ if (effective.errors.length) {
55
+ process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
56
+ process.exit(1);
57
+ }
58
+ if (rest.includes('--json')) {
59
+ process.stdout.write(`${JSON.stringify({ slug, effectiveSpecHash: effective.hash, specs: effective.specs }, null, 2)}\n`);
60
+ } else {
61
+ process.stdout.write(`change: ${slug}\neffective-spec-hash: ${effective.hash}\n`);
62
+ for (const spec of effective.specs) {
63
+ process.stdout.write(`spec: ${spec.capability}\n`);
64
+ for (const req of spec.requirements) process.stdout.write(` ${req.operation === 'BASE' ? '=' : req.operation === 'ADDED' ? '+' : '~'} ${req.id || req.name} [${req.source}]\n`);
65
+ }
66
+ }
67
+ process.exit(0);
68
+ }
69
+
70
+ if (sub === 'migrate') {
71
+ const state = adoptSpecsState(vaultBase);
72
+ process.stdout.write(`spec state adopted: ${Object.keys(state.specs).length} living spec(s); 07-Specs is generated/read-only\n`);
73
+ process.exit(0);
74
+ }
75
+
76
+ if (sub === 'rebase') {
77
+ const slug = option('--change') || activeChange(vaultBase);
78
+ if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
79
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
80
+ try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
81
+ catch { process.stderr.write(`wendkeep spec rebase: change not found: ${slug}\n`); process.exit(2); }
82
+ const capabilities = discoverSpecDeltas(changeDir);
83
+ const conflicts = specConflicts(vaultBase, changeDir, capabilities);
84
+ if (conflicts.length && !rest.includes('--accept-current')) {
85
+ process.stderr.write(`wendkeep spec rebase: conflicts: ${conflicts.join('; ')} — reconcile delta, then rerun with --accept-current\n`);
86
+ process.exit(1);
87
+ }
88
+ captureSpecBaseline(vaultBase, changeDir, { refresh: true });
89
+ process.stdout.write(`spec baseline rebased: ${slug}${conflicts.length ? ` (${conflicts.length} conflict(s) accepted)` : ''}\n`);
90
+ process.exit(0);
91
+ }
92
+
27
93
  if (sub === 'list') {
28
94
  let files = [];
29
95
  try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
@@ -49,6 +115,6 @@ export function runSpec(argv) {
49
115
  process.exit(0);
50
116
  }
51
117
 
52
- process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
118
+ process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show, effective, migrate, rebase.\n`);
53
119
  process.exit(2);
54
120
  }
package/src/sync-defs.mjs CHANGED
@@ -2,11 +2,13 @@
2
2
  // they travel with the project in git. They have no automatic consumer — agents
3
3
  // read them from their own dirs — so `wendkeep sync-defs` copies them there:
4
4
  // .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
5
- // .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
5
+ // .brain/skills/<name>/ -> <project>/.claude/skills/ + .agents/skills/ (skill format)
6
6
  // .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
7
7
  // cross-platform robustness.
8
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
9
- import { isAbsolute, join, resolve } from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
10
12
  import { seedWkSkills } from './skills-seed.mjs';
11
13
  import { getLocale } from '../hooks/locale.mjs';
12
14
 
@@ -15,6 +17,27 @@ import { getLocale } from '../hooks/locale.mjs';
15
17
  // the markers is ours; user content around it is never touched.
16
18
  const AG_START = '<!-- wendkeep:skills:start -->';
17
19
  const AG_END = '<!-- wendkeep:skills:end -->';
20
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
+ const WENDKEEP_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
22
+ const META_FILE = '.wendkeep-meta.json';
23
+
24
+ function directoryHash(root) {
25
+ const hash = createHash('sha256');
26
+ const visit = (dir) => {
27
+ let names = [];
28
+ try { names = readdirSync(dir).sort(); } catch { return; }
29
+ for (const name of names) {
30
+ if (name === META_FILE) continue;
31
+ const path = join(dir, name);
32
+ const rel = relative(root, path).replaceAll('\\', '/');
33
+ const stat = statSync(path);
34
+ if (stat.isDirectory()) visit(path);
35
+ else { hash.update(rel); hash.update('\0'); hash.update(readFileSync(path)); hash.update('\0'); }
36
+ }
37
+ };
38
+ visit(root);
39
+ return hash.digest('hex');
40
+ }
18
41
 
19
42
  function skillInventory(skillsSrc) {
20
43
  const out = [];
@@ -31,9 +54,10 @@ function skillInventory(skillsSrc) {
31
54
  return out;
32
55
  }
33
56
 
34
- function renderAgentsSection(skills) {
57
+ function renderAgentsSection(skills, sourceHash = '') {
35
58
  const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
36
59
  return `${AG_START}
60
+ <!-- wendkeep-version: ${WENDKEEP_VERSION}; skills-sha256: ${sourceHash} -->
37
61
  ## wendkeep — process skills & loop
38
62
 
39
63
  This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
@@ -41,9 +65,10 @@ through its change loop: \`wendkeep change new <slug>\` → implement tasks test
41
65
  (tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
42
66
  \`wendkeep verify --deep\` + an independent read-only verification pass writing
43
67
  \`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
44
- status\` / \`spec list\` / \`sensors list\`.
68
+ status\` / \`spec effective --change <slug>\` / \`sensors list\`. Author specs only in
69
+ \`08-Mudanças/<slug>/specs/\`; \`07-Specs\` is generated and must not be edited directly.
45
70
 
46
- Process skills (full text in \`.claude/skills/\` and the vault's \`.brain/skills/\`):
71
+ Process skills (full text in \`.claude/skills/\`, \`.agents/skills/\`, and the vault's \`.brain/skills/\`):
47
72
  ${list}
48
73
  ${AG_END}`;
49
74
  }
@@ -52,7 +77,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
52
77
  const skills = skillInventory(skillsSrc);
53
78
  if (!skills.length) return false;
54
79
  const path = join(projectPath, 'AGENTS.md');
55
- const section = renderAgentsSection(skills);
80
+ const section = renderAgentsSection(skills, directoryHash(skillsSrc));
56
81
  let content = '';
57
82
  try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
58
83
  if (content.includes(AG_START) && content.includes(AG_END)) {
@@ -67,7 +92,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
67
92
  }
68
93
 
69
94
  export function syncDefs(vaultBase, projectPath) {
70
- const out = { agents: [], skills: [], agentsMd: false };
95
+ const out = { agents: [], skills: [], codexSkills: [], agentsMd: false };
71
96
 
72
97
  const agentsSrc = join(vaultBase, '.brain', 'agents');
73
98
  if (existsSync(agentsSrc)) {
@@ -85,10 +110,19 @@ export function syncDefs(vaultBase, projectPath) {
85
110
  for (const name of readdirSync(skillsSrc)) {
86
111
  const dir = join(skillsSrc, name);
87
112
  if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
88
- const dest = join(projectPath, '.claude', 'skills', name);
89
- mkdirSync(dest, { recursive: true });
90
- cpSync(dir, dest, { recursive: true });
113
+ const destinations = [
114
+ join(projectPath, '.claude', 'skills', name),
115
+ join(projectPath, '.agents', 'skills', name),
116
+ ];
117
+ const sourceHash = directoryHash(dir);
118
+ for (const dest of destinations) {
119
+ rmSync(dest, { recursive: true, force: true });
120
+ mkdirSync(dest, { recursive: true });
121
+ cpSync(dir, dest, { recursive: true });
122
+ writeFileSync(join(dest, META_FILE), `${JSON.stringify({ wendkeepVersion: WENDKEEP_VERSION, sourceHash }, null, 2)}\n`, 'utf8');
123
+ }
91
124
  out.skills.push(name);
125
+ out.codexSkills.push(name);
92
126
  }
93
127
  }
94
128
 
@@ -98,6 +132,33 @@ export function syncDefs(vaultBase, projectPath) {
98
132
  return out;
99
133
  }
100
134
 
135
+ export function checkSyncDefs(vaultBase, projectPath) {
136
+ const issues = [];
137
+ const skillsSrc = join(vaultBase, '.brain', 'skills');
138
+ if (!existsSync(skillsSrc)) return { ok: false, issues: ['fonte .brain/skills ausente'] };
139
+ const names = readdirSync(skillsSrc).filter((name) => {
140
+ try { return statSync(join(skillsSrc, name)).isDirectory(); } catch { return false; }
141
+ });
142
+ for (const name of names) {
143
+ const expected = directoryHash(join(skillsSrc, name));
144
+ for (const relDest of [join('.claude', 'skills', name), join('.agents', 'skills', name)]) {
145
+ const dest = join(projectPath, relDest);
146
+ if (!existsSync(join(dest, 'SKILL.md'))) { issues.push(`${relDest}: ausente`); continue; }
147
+ if (directoryHash(dest) !== expected) issues.push(`${relDest}: conteúdo divergiu da fonte`);
148
+ let meta = null;
149
+ try { meta = JSON.parse(readFileSync(join(dest, META_FILE), 'utf8')); } catch { /* missing */ }
150
+ if (meta?.wendkeepVersion !== WENDKEEP_VERSION || meta?.sourceHash !== expected) {
151
+ issues.push(`${relDest}: metadata stale/ausente (esperado WendKeep ${WENDKEEP_VERSION})`);
152
+ }
153
+ }
154
+ }
155
+ const expectedSection = renderAgentsSection(skillInventory(skillsSrc), directoryHash(skillsSrc));
156
+ let agentsMd = '';
157
+ try { agentsMd = readFileSync(join(projectPath, 'AGENTS.md'), 'utf8'); } catch { /* missing */ }
158
+ if (!agentsMd.includes(expectedSection)) issues.push('AGENTS.md: bloco gerenciado stale/ausente');
159
+ return { ok: issues.length === 0, issues, version: WENDKEEP_VERSION, skills: names.length };
160
+ }
161
+
101
162
  // CLI entry for `wendkeep sync-defs`.
102
163
  export function runSyncDefs(argv) {
103
164
  let vault;
@@ -116,6 +177,15 @@ export function runSyncDefs(argv) {
116
177
  }
117
178
  const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
118
179
  const projectPath = resolve(project || process.cwd());
180
+ if (argv.includes('--check')) {
181
+ const r = checkSyncDefs(vaultBase, projectPath);
182
+ if (r.ok) process.stdout.write(`wendkeep sync-defs --check: ok (${r.skills} skill(s), ${r.version})\n`);
183
+ else {
184
+ process.stderr.write(`wendkeep sync-defs --check: drift detectado\n - ${r.issues.join('\n - ')}\n`);
185
+ process.stderr.write('rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex\n');
186
+ }
187
+ process.exit(r.ok ? 0 : 1);
188
+ }
119
189
  // --reseed (0.31.0): sobrescreve as wk-* de .brain/skills com os seeds da versão instalada
120
190
  // ANTES de copiar — é como um vault existente recebe descriptions/HARD-GATE novos.
121
191
  if (argv.includes('--reseed')) {
@@ -124,7 +194,7 @@ export function runSyncDefs(argv) {
124
194
  }
125
195
  const r = syncDefs(vaultBase, projectPath);
126
196
  process.stdout.write(
127
- `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
197
+ `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills + .agents/skills\n`,
128
198
  );
129
199
  if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
130
200
  if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
@@ -151,8 +221,8 @@ model = "gpt-5.5"
151
221
  const SKILLS_README = `# .brain/skills — versioned custom skill definitions
152
222
 
153
223
  Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
154
- \`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` so
155
- the agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
224
+ \`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` and
225
+ \`<project>/.agents/skills/\`. Edit here (source of truth); re-run sync after changes.
156
226
  `;
157
227
 
158
228
  const EXAMPLE_SKILL = `---
package/src/verify.mjs CHANGED
@@ -5,7 +5,11 @@ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
7
7
  import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
8
- import { tasksHashOf } from '../hooks/spec-core.mjs';
8
+ import {
9
+ buildEffectiveRequirementPackage,
10
+ captureSpecBaseline,
11
+ tasksHashOf,
12
+ } from '../hooks/spec-core.mjs';
9
13
  import { addLesson } from '../hooks/lessons-core.mjs';
10
14
  import { getLocale } from '../hooks/locale.mjs';
11
15
 
@@ -88,16 +92,36 @@ export function runVerify(argv) {
88
92
  const tasks = parseTasks(tarefas);
89
93
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
90
94
  const tasksHash = tasksHashOf(tarefas);
95
+ captureSpecBaseline(vaultBase, changeDir);
96
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
97
+ if (effective.errors.length) {
98
+ process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
99
+ process.exit(1);
100
+ }
101
+ if (effective.missing.length) {
102
+ process.stderr.write(`verify --deep: requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}\n`);
103
+ process.exit(1);
104
+ }
91
105
  const pkg = {
92
106
  slug,
93
107
  tasksHash,
94
- requirements: reqIds.map((id) => ({ id })),
108
+ effectiveSpecHash: effective.hash,
109
+ requirements: effective.requirements.map((req) => {
110
+ return {
111
+ id: req.id,
112
+ name: req.name,
113
+ capability: req.capability,
114
+ operation: req.operation,
115
+ source: req.source,
116
+ body: req.body,
117
+ };
118
+ }),
95
119
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
96
120
  sensors: evidence,
97
121
  };
98
122
  writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
99
123
  if (reqIds.length === 0) {
100
- writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
124
+ writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
101
125
  process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
102
126
  } else {
103
127
  process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');