wendkeep 0.7.0 → 0.8.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,26 @@ 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.8.0] — 2026-07-05
8
+
9
+ Reach: internationalization + agent-agnostic distribution.
10
+
11
+ ### Added
12
+ - **Vault locale (i18n):** `wendkeep init --locale en` creates an English vault
13
+ (`02-Sessions`, `04-Decisions`, `08-Changes`, …, English months, English change scaffold,
14
+ English CORE skeleton, localized theme/graph groups). The locale is a vault property
15
+ (`.brain/config.json`), locked at init; absent = `pt-BR` — existing vaults are untouched
16
+ and never renamed. Parsers are **bilingual everywhere** (`Requisito|Requirement`,
17
+ `mata mutante|kill mutant`, CORE section sets), so mixed content never breaks.
18
+ - **AGENTS.md managed section:** `sync-defs`/`init` maintain a marker-delimited section in
19
+ the project's `AGENTS.md` (loop summary + skill inventory) — one file that Codex, Amp,
20
+ Cursor, Zed and any AGENTS.md-reading agent picks up. User content around it is preserved.
21
+ - **Harness contract v1.2** (`docs/14`): locale + AGENTS.md channel.
22
+
23
+ ### Deferred
24
+ - Extra mutation-report formats (mutmut/PIT) and per-agent session-hook wiring — backlog
25
+ (`docs/17`).
26
+
7
27
  ## [0.7.0] — 2026-07-05
8
28
 
9
29
  Ergonomics: the loop without hand-editing files.
@@ -160,6 +180,7 @@ Initial release — the capture engine, extracted from a system in daily product
160
180
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
161
181
 
162
182
  <!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
183
+ [0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
163
184
  [0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
164
185
  [0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
165
186
  [0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
@@ -3,6 +3,7 @@
3
3
  import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
6
+ import { getLocale } from './locale.mjs';
6
7
 
7
8
  export function brainDir(vaultBase) {
8
9
  return join(vaultBase, '.brain');
@@ -60,7 +61,7 @@ function derivedLinks(content) {
60
61
  // Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
61
62
  export function buildBrainIndex(vaultBase) {
62
63
  const rows = [];
63
- for (const fp of walkMd(join(vaultBase, '02-Sessões'))) {
64
+ for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
64
65
  let content;
65
66
  try { content = readFileSync(fp, 'utf8'); } catch { continue; }
66
67
  const fm = parseFrontmatter(content);
@@ -2,20 +2,20 @@
2
2
  // Native change/spec lifecycle in the vault (Pilar B). Vault-facing lib consumed by
3
3
  // the `wendkeep change` CLI (src/change.mjs) and the brain-inject hook. No external deps.
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
5
- import { join } from 'node:path';
5
+ import { dirname, join } from 'node:path';
6
6
  import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
7
7
  import { parseSpecsList, promoteSpecs } from './spec-core.mjs';
8
+ import { getLocale } from './locale.mjs';
8
9
 
9
- export const CHANGES_DIR = '08-Mudanças';
10
- export const SPECS_DIR = '07-Specs';
11
10
  export const ARCHIVE_DIR = '_arquivo';
12
11
  const POINTER = '.brain/CURRENT_CHANGE.md';
13
12
 
14
- export function changeDirRel(slug) {
15
- return join(CHANGES_DIR, slug);
13
+ export function changeDirRel(slug, vaultBase) {
14
+ return join(getLocale(vaultBase).folders.changes, slug);
16
15
  }
17
16
 
18
- export function renderChangeScaffold({ slug, sessionRel, dateStr }) {
17
+ export function renderChangeScaffold({ slug, sessionRel, dateStr, locale = 'pt-BR' }) {
18
+ const en = locale === 'en';
19
19
  const source = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
20
20
  const proposta = `---
21
21
  type: change
@@ -31,27 +31,20 @@ specs: []
31
31
 
32
32
  # ${slug}
33
33
 
34
- ## Por quê
35
-
36
- (motivo da mudança)
37
-
38
- ## O que muda
39
-
40
- (escopo da mudança)
34
+ ${en ? '## Why\n\n(reason for the change)\n\n## What changes\n\n(scope of the change)' : '## Por quê\n\n(motivo da mudança)\n\n## O que muda\n\n(escopo da mudança)'}
41
35
  `;
42
36
  const design = `# ${slug} — design
43
37
 
44
- ## Abordagem
45
-
46
- (abordagem técnica)
38
+ ${en ? '## Approach\n\n(technical approach)' : '## Abordagem\n\n(abordagem técnica)'}
47
39
  `;
48
- const tarefas = `# ${slug} — tarefas
40
+ const tarefas = `# ${slug} — ${en ? 'tasks' : 'tarefas'}
49
41
 
50
- - [ ] 1.1 (primeira tarefa)
42
+ - [ ] 1.1 ${en ? '(first task)' : '(primeira tarefa)'}
51
43
  `;
44
+ const reqHeading = en ? 'Requirement' : 'Requisito';
52
45
  const specDelta = `## ADDED Requirements
53
- ### Requisito: (nome)
54
- (comportamento / cenários)
46
+ ### ${reqHeading}: ${en ? '(name)' : '(nome)'}
47
+ ${en ? '(behaviour / scenarios)' : '(comportamento / cenários)'}
55
48
 
56
49
  ## MODIFIED Requirements
57
50
 
@@ -80,10 +73,11 @@ export function clearActiveChange(vaultBase) {
80
73
  }
81
74
 
82
75
  export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple = false }) {
83
- const dir = join(vaultBase, CHANGES_DIR, slug);
76
+ const loc = getLocale(vaultBase);
77
+ const dir = join(vaultBase, loc.folders.changes, slug);
84
78
  const existed = existsSync(join(dir, 'proposta.md'));
85
79
  mkdirSync(dir, { recursive: true });
86
- const files = renderChangeScaffold({ slug, sessionRel, dateStr });
80
+ const files = renderChangeScaffold({ slug, sessionRel, dateStr, locale: loc.id });
87
81
  const write = (name, content) => {
88
82
  const f = join(dir, name);
89
83
  if (!existsSync(f)) writeFileSync(f, content, 'utf8');
@@ -100,7 +94,7 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
100
94
  }
101
95
  }
102
96
  setActiveChange(vaultBase, slug);
103
- return { rel: changeDirRel(slug), created: !existed };
97
+ return { rel: changeDirRel(slug, vaultBase), created: !existed };
104
98
  }
105
99
 
106
100
  export function parseTasks(md) {
@@ -135,7 +129,7 @@ export function setTaskDone(changeDir, taskId, done = true) {
135
129
  }
136
130
 
137
131
  export function listChanges(vaultBase) {
138
- const base = join(vaultBase, CHANGES_DIR);
132
+ const base = join(vaultBase, getLocale(vaultBase).folders.changes);
139
133
  const active = [];
140
134
  let archived = [];
141
135
  try {
@@ -153,29 +147,33 @@ export function listChanges(vaultBase) {
153
147
  export function buildActiveChangeInjection(vaultBase, { maxTasks = 8 } = {}) {
154
148
  const slug = activeChange(vaultBase);
155
149
  if (!slug) return '';
150
+ const chDir = getLocale(vaultBase).folders.changes;
156
151
  let md = '';
157
- try { md = readFileSync(join(vaultBase, CHANGES_DIR, slug, 'tarefas.md'), 'utf8'); } catch { return ''; }
152
+ try { md = readFileSync(join(vaultBase, chDir, slug, 'tarefas.md'), 'utf8'); } catch { return ''; }
158
153
  const open = parseTasks(md).filter((t) => !t.done).slice(0, maxTasks);
159
154
  const lines = open.map((t) => `- [ ] ${t.id} ${t.text}`);
160
155
  const more = open.length === maxTasks ? '\n*…mais tarefas em tarefas.md*' : '';
161
156
  return `<active_change>
162
- Mudança ativa: ${slug} — [[${CHANGES_DIR}/${slug}/proposta]]. Tarefas abertas:
157
+ Mudança ativa: ${slug} — [[${chDir}/${slug}/proposta]]. Tarefas abertas:
163
158
  ${lines.join('\n')}${more}
164
159
  </active_change>`;
165
160
  }
166
161
 
167
162
  export function activeChangeLink(vaultBase) {
168
163
  const slug = activeChange(vaultBase);
169
- return slug ? `Change ativa: [[${CHANGES_DIR}/${slug}/proposta]]` : '';
164
+ return slug ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${slug}/proposta]]` : '';
170
165
  }
171
166
 
172
167
  // Append fix tasks for surviving mutants to a change's tarefas.md (Wave B). Deduped by
173
168
  // file:line, numbered M.<n> continuing from any existing fix tasks. Returns count added.
174
169
  export function appendFixTasks(changeDir, mutants, sensorId) {
175
170
  const path = join(changeDir, 'tarefas.md');
171
+ // changeDir = <vault>/<changesDir>/<slug> — derive the vault for the locale verb.
172
+ const verb = getLocale(dirname(dirname(changeDir))).fixTaskVerb;
176
173
  let md = '';
177
174
  try { md = readFileSync(path, 'utf8'); } catch { /* nova */ }
178
- const existing = new Set([...md.matchAll(/mata mutante (\S+):(\d+)/g)].map((m) => `${m[1]}:${m[2]}`));
175
+ // Dedup is bilingual so a vault that switched locale mid-change never duplicates.
176
+ const existing = new Set([...md.matchAll(/(?:mata mutante|kill mutant) (\S+):(\d+)/g)].map((m) => `${m[1]}:${m[2]}`));
179
177
  const nums = [...md.matchAll(/^-\s+\[[ x]\]\s+M\.(\d+)\b/gm)].map((m) => Number(m[1]));
180
178
  let n = nums.length ? Math.max(...nums) : 0;
181
179
  const lines = [];
@@ -184,7 +182,7 @@ export function appendFixTasks(changeDir, mutants, sensorId) {
184
182
  if (existing.has(key)) continue;
185
183
  existing.add(key);
186
184
  n += 1;
187
- lines.push(`- [ ] M.${n} mata mutante ${mut.file}:${mut.line} (${mut.mutator}) [sensor:${sensorId}]`);
185
+ lines.push(`- [ ] M.${n} ${verb} ${mut.file}:${mut.line} (${mut.mutator}) [sensor:${sensorId}]`);
188
186
  }
189
187
  if (!lines.length) return 0;
190
188
  const sep = md === '' || md.endsWith('\n') ? '' : '\n';
@@ -198,11 +196,13 @@ export function gateGreen() {
198
196
  }
199
197
 
200
198
  export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrNum }) {
201
- const src = join(vaultBase, CHANGES_DIR, slug);
199
+ const loc = getLocale(vaultBase);
200
+ const chDir = loc.folders.changes;
201
+ const src = join(vaultBase, chDir, slug);
202
202
  const verdict = gate(src);
203
203
  if (!verdict.ok) return { ok: false, failing: verdict.failing || [] };
204
204
 
205
- const destRel = join(CHANGES_DIR, ARCHIVE_DIR, `${dateStr}-${slug}`);
205
+ const destRel = join(chDir, ARCHIVE_DIR, `${dateStr}-${slug}`);
206
206
  const changeWikilink = wikilinkFromRel(join(destRel, 'proposta'));
207
207
 
208
208
  // Promote spec deltas into the living 07-Specs BEFORE moving (deltas live in src).
@@ -220,16 +220,16 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
220
220
  let reqIds = [];
221
221
  try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
222
222
 
223
- ensureDir(join(vaultBase, CHANGES_DIR, ARCHIVE_DIR));
223
+ ensureDir(join(vaultBase, chDir, ARCHIVE_DIR));
224
224
  renameSync(src, join(vaultBase, destRel));
225
225
 
226
226
  const [year] = String(dateStr).split('-');
227
- const adrDirRel = join('04-Decisões', year);
227
+ const adrDirRel = join(loc.folders.decisions, year);
228
228
  ensureDir(join(vaultBase, adrDirRel));
229
229
  const num = String(adrNum).padStart(3, '0');
230
230
  const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
231
231
  const capLine = promoted.length
232
- ? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(SPECS_DIR, c))).join(', ')}.`
232
+ ? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(loc.folders.specs, c))).join(', ')}.`
233
233
  : '';
234
234
  const reqLine = reqIds.length ? `\n\nRequisitos: ${reqIds.join(', ')}.` : '';
235
235
  writeFileSync(join(vaultBase, adrRel), `---
@@ -4,11 +4,12 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { activeChange, parseTasks } from './change-core.mjs';
6
6
  import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict } from './spec-core.mjs';
7
-
8
- const CHANGES_DIR = '08-Mudanças';
9
- const SPECS_DIR = '07-Specs';
7
+ import { getLocale } from './locale.mjs';
10
8
 
11
9
  export function checkHarness(vaultBase, projectRoot) {
10
+ const loc = getLocale(vaultBase);
11
+ const CHANGES_DIR = loc.folders.changes;
12
+ const SPECS_DIR = loc.folders.specs;
12
13
  const errors = [];
13
14
  const warnings = [];
14
15
 
@@ -12,6 +12,7 @@ import {
12
12
  toVaultRelative,
13
13
  wikilinkFromRel,
14
14
  } from './obsidian-common.mjs';
15
+ import { getLocale } from './locale.mjs';
15
16
 
16
17
  function yamlQuote(value) {
17
18
  return `"${String(value || '').replaceAll('"', '\\"')}"`;
@@ -446,7 +447,7 @@ _Registrar como este conhecimento pode ser reutilizado._
446
447
  `;
447
448
  }
448
449
 
449
- const DERIVED_FOLDERS = { bugs: '05-Bugs', decisions: '04-Decisões', learnings: '06-Aprendizados' };
450
+ const derivedFoldersFor = (vaultBase) => { const f = getLocale(vaultBase).folders; return { bugs: f.bugs, decisions: f.decisions, learnings: f.learnings }; };
450
451
 
451
452
  function listMd(dir) {
452
453
  try { return readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { return []; }
@@ -456,8 +457,8 @@ function listMd(dir) {
456
457
  function existingKeysForSession(vaultBase, sessionRel, dateStr) {
457
458
  const wikilink = wikilinkFromRel(sessionRel);
458
459
  const out = { bugs: [], decisions: [], learnings: [] };
459
- for (const [type, folder] of Object.entries(DERIVED_FOLDERS)) {
460
- const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr));
460
+ for (const [type, folder] of Object.entries(derivedFoldersFor(vaultBase))) {
461
+ const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr, vaultBase));
461
462
  for (const fileName of listMd(dir)) {
462
463
  try {
463
464
  const c = readFileSync(join(dir, fileName), 'utf-8');
@@ -477,9 +478,10 @@ function alreadyHasKey(keys, candidate) {
477
478
  export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options = {}) {
478
479
  const linked = { decisions: [], bugs: [], learnings: [] };
479
480
  const provider = providerMeta(options.provider);
480
- const bugsDir = join(vaultBase, monthFolderRelFromDateStr('05-Bugs', dateStr));
481
- const decisionsDir = join(vaultBase, monthFolderRelFromDateStr('04-Decisões', dateStr));
482
- const learningsDir = join(vaultBase, monthFolderRelFromDateStr('06-Aprendizados', dateStr));
481
+ const locF = getLocale(vaultBase).folders;
482
+ const bugsDir = join(vaultBase, monthFolderRelFromDateStr(locF.bugs, dateStr, vaultBase));
483
+ const decisionsDir = join(vaultBase, monthFolderRelFromDateStr(locF.decisions, dateStr, vaultBase));
484
+ const learningsDir = join(vaultBase, monthFolderRelFromDateStr(locF.learnings, dateStr, vaultBase));
483
485
  ensureDir(bugsDir);
484
486
  ensureDir(decisionsDir);
485
487
  ensureDir(learningsDir);
@@ -0,0 +1,73 @@
1
+ // hooks/locale.mjs — vault locale (i18n, 0.8.0). The locale is a property of the VAULT,
2
+ // stored at <vault>/.brain/config.json ({ "locale": "en" }); absent = pt-BR (full backward
3
+ // compat). Parsers stay bilingual everywhere; only RENDERING follows the locale.
4
+ import { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export const LOCALES = {
8
+ 'pt-BR': {
9
+ id: 'pt-BR',
10
+ folders: {
11
+ inbox: '00-Inbox',
12
+ project: '01-Projeto',
13
+ sessions: '02-Sessões',
14
+ linear: '03-Linear',
15
+ decisions: '04-Decisões',
16
+ bugs: '05-Bugs',
17
+ learnings: '06-Aprendizados',
18
+ specs: '07-Specs',
19
+ changes: '08-Mudanças',
20
+ },
21
+ months: ['01-JAN', '02-FEV', '03-MAR', '04-ABR', '05-MAI', '06-JUN', '07-JUL', '08-AGO', '09-SET', '10-OUT', '11-NOV', '12-DEZ'],
22
+ reqHeading: 'Requisito',
23
+ fixTaskVerb: 'mata mutante',
24
+ coreSections: ['Preferências do Usuário', 'Padrões Ativos', 'Pendências Abertas'],
25
+ },
26
+ en: {
27
+ id: 'en',
28
+ folders: {
29
+ inbox: '00-Inbox',
30
+ project: '01-Project',
31
+ sessions: '02-Sessions',
32
+ linear: '03-Linear',
33
+ decisions: '04-Decisions',
34
+ bugs: '05-Bugs',
35
+ learnings: '06-Learnings',
36
+ specs: '07-Specs',
37
+ changes: '08-Changes',
38
+ },
39
+ months: ['01-JAN', '02-FEB', '03-MAR', '04-APR', '05-MAY', '06-JUN', '07-JUL', '08-AUG', '09-SEP', '10-OCT', '11-NOV', '12-DEC'],
40
+ reqHeading: 'Requirement',
41
+ fixTaskVerb: 'kill mutant',
42
+ coreSections: ['User Preferences', 'Active Patterns', 'Open Items'],
43
+ },
44
+ };
45
+
46
+ export const DEFAULT_LOCALE = 'pt-BR';
47
+
48
+ // Per-process cache: one vault per process (hooks + CLI), reads are hot paths.
49
+ const cache = new Map();
50
+
51
+ export function getLocale(vaultBase) {
52
+ if (!vaultBase) return LOCALES[DEFAULT_LOCALE];
53
+ const key = String(vaultBase);
54
+ if (cache.has(key)) return cache.get(key);
55
+ let id = DEFAULT_LOCALE;
56
+ try {
57
+ const data = JSON.parse(readFileSync(join(key, '.brain', 'config.json'), 'utf8'));
58
+ if (data.locale && LOCALES[data.locale]) id = data.locale;
59
+ } catch { /* sem config = pt-BR */ }
60
+ const loc = LOCALES[id];
61
+ cache.set(key, loc);
62
+ return loc;
63
+ }
64
+
65
+ // Test hook: drop the memoized entry (tests rewrite config.json under one tmpdir).
66
+ export function clearLocaleCache() {
67
+ cache.clear();
68
+ }
69
+
70
+ // The full vault taxonomy for a locale (folders + fixed entries), in creation order.
71
+ export function vaultFolders(loc) {
72
+ return [...Object.values(loc.folders), 'Templates', '.brain'];
73
+ }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'fs';
3
3
  import { basename, dirname, join, relative } from 'path';
4
+ import { getLocale } from './locale.mjs';
4
5
 
5
6
  // Neutral fallback only. The vault is normally resolved from the
6
7
  // OBSIDIAN_VAULT_PATH env var (set by `wendkeep init`) via getVaultBase() below.
@@ -128,26 +129,28 @@ export function formatLocalIso(date = new Date()) {
128
129
  return `${formatDate(date)}T${formatTime(date)}`;
129
130
  }
130
131
 
131
- export function datedFolderRel(rootFolder, date = new Date()) {
132
+ // Locale (0.8.0): month labels + folder names come from the vault locale when a
133
+ // vaultBase is given; without it, pt-BR (backward compat — every legacy caller).
134
+ export function datedFolderRel(rootFolder, date = new Date(), vaultBase) {
132
135
  const p = localDateParts(date);
133
- return join(rootFolder, String(p.year), MONTH_FOLDERS[p.month - 1], `DIA ${pad2(p.day)}`);
136
+ return join(rootFolder, String(p.year), getLocale(vaultBase).months[p.month - 1], `DIA ${pad2(p.day)}`);
134
137
  }
135
138
 
136
139
  // Mesma estrutura datada a partir de uma string 'YYYY-MM-DD' (sessões: até o DIA).
137
- export function datedFolderRelFromDateStr(rootFolder, dateStr) {
140
+ export function datedFolderRelFromDateStr(rootFolder, dateStr, vaultBase) {
138
141
  const [year, month, day] = String(dateStr).split('-');
139
- return join(rootFolder, year, MONTH_FOLDERS[Number(month) - 1], `DIA ${pad2(day)}`);
142
+ return join(rootFolder, year, getLocale(vaultBase).months[Number(month) - 1], `DIA ${pad2(day)}`);
140
143
  }
141
144
 
142
145
  // Estrutura até o MÊS (sem DIA) — usada pelas notas derivadas (decisões/bugs/
143
146
  // aprendizados): tudo do mês fica junto em <pasta>/<ano>/<MM-MMM>/.
144
- export function monthFolderRelFromDateStr(rootFolder, dateStr) {
147
+ export function monthFolderRelFromDateStr(rootFolder, dateStr, vaultBase) {
145
148
  const [year, month] = String(dateStr).split('-');
146
- return join(rootFolder, year, MONTH_FOLDERS[Number(month) - 1]);
149
+ return join(rootFolder, year, getLocale(vaultBase).months[Number(month) - 1]);
147
150
  }
148
151
 
149
- export function sessionFolderRel(date = new Date()) {
150
- return datedFolderRel('02-Sessões', date);
152
+ export function sessionFolderRel(date = new Date(), vaultBase) {
153
+ return datedFolderRel(getLocale(vaultBase).folders.sessions, date, vaultBase);
151
154
  }
152
155
 
153
156
  export function controlPath(vaultBase) {
@@ -532,7 +535,7 @@ export function listMarkdownFiles(dir) {
532
535
  }
533
536
 
534
537
  export function getNextAdrNumber(vaultBase) {
535
- const decisionsDir = join(vaultBase, '04-Decisões');
538
+ const decisionsDir = join(vaultBase, getLocale(vaultBase).folders.decisions);
536
539
  let max = 0;
537
540
  // Varre recursivamente: os ADRs agora vivem em subpastas datadas (AAAA/MM-MMM/DIA DD).
538
541
  const walk = (dir) => {
@@ -115,7 +115,7 @@ Sessão ainda em andamento.
115
115
  }
116
116
 
117
117
  function allocateSessionPath(vaultBase, now, summary = 'session') {
118
- const folderRel = sessionFolderRel(now);
118
+ const folderRel = sessionFolderRel(now, vaultBase);
119
119
  const folderAbs = join(vaultBase, folderRel);
120
120
  ensureDir(folderAbs);
121
121
 
@@ -113,7 +113,7 @@ Sessão ainda em andamento.
113
113
  }
114
114
 
115
115
  function allocateSessionPath(vaultBase, now, summary = 'session') {
116
- const folderRel = sessionFolderRel(now);
116
+ const folderRel = sessionFolderRel(now, vaultBase);
117
117
  const folderAbs = join(vaultBase, folderRel);
118
118
  ensureDir(folderAbs);
119
119
 
@@ -7,6 +7,7 @@ import { createLinkedNotes } from './linked-notes.mjs';
7
7
  import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normalizeCodexUsage, priceForModel, updateSessionUsage } from './token-usage.mjs';
8
8
  import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
9
9
  import { activeChangeLink } from './change-core.mjs';
10
+ import { getLocale } from './locale.mjs';
10
11
  import {
11
12
  ensureDir,
12
13
  findActiveSessionByTranscript,
@@ -781,10 +782,11 @@ function noteReferencesSession(content, sessionRel) {
781
782
 
782
783
  function findLinkedDerivedNotes(vaultBase, sessionRel) {
783
784
  const linked = { decisions: [], bugs: [], learnings: [] };
785
+ const locF = getLocale(vaultBase).folders;
784
786
  const folders = {
785
- decisions: '04-Decisões',
786
- bugs: '05-Bugs',
787
- learnings: '06-Aprendizados',
787
+ decisions: locF.decisions,
788
+ bugs: locF.bugs,
789
+ learnings: locF.learnings,
788
790
  };
789
791
 
790
792
  for (const [key, folder] of Object.entries(folders)) {
@@ -4,14 +4,15 @@ import { createHash } from 'node:crypto';
4
4
  import { readFileSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { ensureDir } from './obsidian-common.mjs';
7
+ import { getLocale } from './locale.mjs';
7
8
 
8
9
  // Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
9
10
  export function tasksHashOf(md) {
10
11
  return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
11
12
  }
12
13
 
13
- const SPECS_DIR = '07-Specs';
14
- const REQ_RE = /^### Requisito:\s*(.+)$/gm;
14
+ // Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
15
+ const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
15
16
 
16
17
  export function parseRequirements(md) {
17
18
  const text = String(md);
@@ -71,8 +72,8 @@ export function applyDelta(reqs, delta) {
71
72
  return { reqs: out, warnings };
72
73
  }
73
74
 
74
- export function renderSpec(capability, reqs, { footer } = {}) {
75
- const blocks = reqs.map((r) => `### Requisito: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
75
+ export function renderSpec(capability, reqs, { footer, reqHeading = 'Requisito' } = {}) {
76
+ const blocks = reqs.map((r) => `### ${reqHeading}: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
76
77
  const foot = footer ? `\n\n> ${footer}\n` : '\n';
77
78
  return `---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
78
79
  }
@@ -88,20 +89,22 @@ export function parseSpecsList(propostaMd) {
88
89
 
89
90
  // Merge each capability's delta (in the change) into the living spec in 07-Specs.
90
91
  export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, dateStr } = {}) {
92
+ const loc = getLocale(vaultBase);
93
+ const specsDir = loc.folders.specs;
91
94
  const promoted = [];
92
95
  const warnings = [];
93
96
  for (const cap of specs) {
94
97
  let delta;
95
98
  try { delta = parseDelta(readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8')); }
96
99
  catch { warnings.push(`sem delta para ${cap}`); continue; }
97
- const livePath = join(vaultBase, SPECS_DIR, `${cap}.md`);
100
+ const livePath = join(vaultBase, specsDir, `${cap}.md`);
98
101
  let current = [];
99
102
  try { current = parseRequirements(readFileSync(livePath, 'utf8')); } catch { /* nova capability */ }
100
103
  const applied = applyDelta(current, delta);
101
104
  warnings.push(...applied.warnings.map((w) => `${cap}: ${w}`));
102
- ensureDir(join(vaultBase, SPECS_DIR));
105
+ ensureDir(join(vaultBase, specsDir));
103
106
  const footer = changeWikilink ? `Atualizado por ${changeWikilink} em ${dateStr}.` : '';
104
- writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer }), 'utf8');
107
+ writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer, reqHeading: loc.reqHeading }), 'utf8');
105
108
  promoted.push(cap);
106
109
  }
107
110
  return { promoted, warnings };
@@ -10,6 +10,7 @@ import {
10
10
  readSessionRegistry,
11
11
  wikilinkFromRel,
12
12
  } from './obsidian-common.mjs';
13
+ import { getLocale } from './locale.mjs';
13
14
 
14
15
  const DEFAULT_PENDING_PATTERNS = [
15
16
  /^- \[ \] Revisar resumo da sessão$/i,
@@ -164,7 +165,8 @@ export function runVaultHealth({ vaultBase, session = '' }) {
164
165
  .length;
165
166
  if (staleDone) warnings.push(`${staleDone} entradas active com ended_at no SESSION_REGISTRY.`);
166
167
 
167
- const derivedFolders = ['04-Decisões', '05-Bugs', '06-Aprendizados'];
168
+ const locF = getLocale(vaultBase).folders;
169
+ const derivedFolders = [locF.decisions, locF.bugs, locF.learnings];
168
170
  const derivedCount = derivedFolders.reduce((total, folder) => {
169
171
  const dir = join(vaultBase, folder);
170
172
  return total + (existsSync(dir) ? listMarkdownFiles(dir).length : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/change.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
13
13
  import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
14
14
  import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
15
+ import { getLocale } from '../hooks/locale.mjs';
15
16
 
16
17
  function resolveVault(argv) {
17
18
  let vault;
@@ -69,7 +70,7 @@ export function runChange(argv) {
69
70
  const slug = slugArg();
70
71
  if (!slug) { process.stderr.write('wendkeep change show: missing <slug>\n'); process.exit(2); }
71
72
  let md;
72
- try { md = readFileSync(join(vaultBase, '08-Mudanças', slug, 'tarefas.md'), 'utf8'); }
73
+ try { md = readFileSync(join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'tarefas.md'), 'utf8'); }
73
74
  catch { process.stderr.write(`wendkeep change show: not found: ${slug}\n`); process.exit(2); }
74
75
  const tasks = parseTasks(md);
75
76
  const open = tasks.filter((t) => !t.done).length;
@@ -81,7 +82,7 @@ export function runChange(argv) {
81
82
  if (sub === 'status') {
82
83
  const slug = slugArg() || activeChange(vaultBase);
83
84
  if (!slug) { process.stderr.write('wendkeep change status: no change (arg or active)\n'); process.exit(2); }
84
- const dir = join(vaultBase, '08-Mudanças', slug);
85
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
85
86
  let tarefasMd;
86
87
  try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); }
87
88
  catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
@@ -117,7 +118,7 @@ export function runChange(argv) {
117
118
  if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
118
119
  const slug = opt(rest, '--change') || activeChange(vaultBase);
119
120
  if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
120
- const dir = join(vaultBase, '08-Mudanças', slug);
121
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
121
122
  let ok = false;
122
123
  try { ok = setTaskDone(dir, taskId, sub === 'done'); } catch { /* sem tarefas.md */ }
123
124
  if (!ok) { process.stderr.write(`wendkeep change ${sub}: task não encontrada: ${taskId}\n`); process.exit(2); }
@@ -128,7 +129,7 @@ export function runChange(argv) {
128
129
  if (sub === 'diff') {
129
130
  const slug = slugArg() || activeChange(vaultBase);
130
131
  if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
131
- const dir = join(vaultBase, '08-Mudanças', slug);
132
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
132
133
  let specs = [];
133
134
  try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); }
134
135
  catch { process.stderr.write(`wendkeep change diff: not found: ${slug}\n`); process.exit(2); }
@@ -142,7 +143,7 @@ export function runChange(argv) {
142
143
  for (const r of delta.modified) process.stdout.write(` ~ ${r.id || r.name} (MODIFIED)\n`);
143
144
  for (const k of delta.removed) process.stdout.write(` - ${k} (REMOVED)\n`);
144
145
  let living = [];
145
- try { living = parseRequirements(readFileSync(join(vaultBase, '07-Specs', `${cap}.md`), 'utf8')); } catch { /* nova capability */ }
146
+ try { living = parseRequirements(readFileSync(join(vaultBase, getLocale(vaultBase).folders.specs, `${cap}.md`), 'utf8')); } catch { /* nova capability */ }
146
147
  for (const w of applyDelta(living, delta).warnings) process.stdout.write(` ! ${w}\n`);
147
148
  }
148
149
  process.exit(0);
package/src/init.mjs CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
34
34
  import { seedDefinitions, syncDefs } from './sync-defs.mjs';
35
35
  import { seedWkSkills } from './skills-seed.mjs';
36
+ import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
36
37
  import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
37
38
 
38
39
  function parseArgs(argv) {
@@ -40,6 +41,8 @@ function parseArgs(argv) {
40
41
  for (let i = 0; i < argv.length; i += 1) {
41
42
  const a = argv[i];
42
43
  if (a === '--vault') args.vault = argv[++i];
44
+ else if (a === '--locale') args.locale = argv[++i];
45
+ else if (a.startsWith('--locale=')) args.locale = a.slice(9);
43
46
  else if (a === '--project') args.project = argv[++i];
44
47
  else if (a === '--no-mcp') args.mcp = false;
45
48
  else if (a === '--yes' || a === '-y') args.yes = true;
@@ -162,10 +165,11 @@ function runCavemanInstaller(log) {
162
165
  // enable it (non-destructive), and add graph color groups. Returns a short note.
163
166
  // Unparseable user JSON is left untouched (we only merge into valid/absent files).
164
167
  function installVaultColors(vaultPath) {
168
+ const loc = getLocale(vaultPath);
165
169
  const obsidianDir = join(vaultPath, '.obsidian');
166
170
  const snippetsDir = join(obsidianDir, 'snippets');
167
171
  mkdirSync(snippetsDir, { recursive: true });
168
- writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(), 'utf8');
172
+ writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(loc), 'utf8');
169
173
 
170
174
  let enabled = false;
171
175
  const appPath = join(obsidianDir, 'appearance.json');
@@ -179,9 +183,9 @@ function installVaultColors(vaultPath) {
179
183
  const graphPath = join(obsidianDir, 'graph.json');
180
184
  const graphRead = readJsonSafe(graphPath);
181
185
  if (graphRead.ok) {
182
- const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups());
186
+ const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups(loc));
183
187
  writeJson(graphPath, merged);
184
- groups = graphColorGroups().length;
188
+ groups = graphColorGroups(loc).length;
185
189
  }
186
190
  return `snippet ${SNIPPET_NAME}.css${enabled ? ' (enabled)' : ' (enable by hand: appearance.json unreadable)'} + ${groups} graph group(s)`;
187
191
  }
@@ -253,9 +257,21 @@ export async function runInit(argv) {
253
257
  }
254
258
 
255
259
  // 1. Vault taxonomy ---------------------------------------------------------
260
+ // Locale (0.8.0): a vault property, locked at init. Written BEFORE folder creation so
261
+ // the folder names follow it. Invalid/absent = pt-BR (backward compat).
256
262
  if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
263
+ const localeId = args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE;
264
+ if (args.locale && !LOCALES[args.locale]) {
265
+ log(` ! locale desconhecido "${args.locale}" — usando ${DEFAULT_LOCALE} (opções: ${Object.keys(LOCALES).join(', ')})`);
266
+ }
267
+ mkdirSync(join(vaultPath, '.brain'), { recursive: true });
268
+ const configPath = join(vaultPath, '.brain', 'config.json');
269
+ if (!existsSync(configPath)) writeFileSync(configPath, `${JSON.stringify({ locale: localeId }, null, 2)}\n`, 'utf8');
270
+ clearLocaleCache();
271
+ const loc = getLocale(vaultPath);
272
+ const folders = vaultFolders(loc);
257
273
  let created = 0;
258
- for (const f of VAULT_FOLDERS) {
274
+ for (const f of folders) {
259
275
  const p = join(vaultPath, f);
260
276
  if (!existsSync(p)) {
261
277
  mkdirSync(p, { recursive: true });
@@ -275,7 +291,7 @@ export async function runInit(argv) {
275
291
  const brainDir = join(vaultPath, '.brain');
276
292
  mkdirSync(brainDir, { recursive: true });
277
293
  const corePath = join(brainDir, 'CORE.md');
278
- if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
294
+ if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(loc.id), 'utf8');
279
295
  const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
280
296
  if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
281
297
  // Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
@@ -283,8 +299,8 @@ export async function runInit(argv) {
283
299
  seedDefinitions(brainDir);
284
300
  seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
285
301
  // Seed the change/spec layer starters (Pilar B) — non-destructive.
286
- const specsReadme = join(vaultPath, '07-Specs', 'README.md');
287
- if (!existsSync(specsReadme)) writeFileSync(specsReadme, '# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em `08-Mudanças/` promovem deltas aqui no `wendkeep change archive`.\n', 'utf8');
302
+ const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
303
+ if (!existsSync(specsReadme)) writeFileSync(specsReadme, `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
288
304
  const changeTpl = join(vaultPath, 'Templates', 'Change.md');
289
305
  if (!existsSync(changeTpl)) writeFileSync(changeTpl, '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
290
306
  // Seed the native sensor config (Pilar C) at project root — non-destructive.
@@ -294,7 +310,7 @@ export async function runInit(argv) {
294
310
  try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
295
311
  writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
296
312
  }
297
- log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain + change/spec + sensors seeded`);
313
+ log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded`);
298
314
  // Deliver the seeded defs (agents + wk process skills) to the project so they're
299
315
  // usable immediately — no separate `wendkeep sync-defs` step needed.
300
316
  const synced = syncDefs(vaultPath, projectPath);
package/src/spec.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import { parseRequirements } from '../hooks/spec-core.mjs';
5
+ import { getLocale } from '../hooks/locale.mjs';
5
6
 
6
7
  function resolveVault(argv) {
7
8
  let vault;
@@ -21,7 +22,7 @@ function resolveVault(argv) {
21
22
  export function runSpec(argv) {
22
23
  const [sub, ...rest] = argv;
23
24
  const vaultBase = resolveVault(rest);
24
- const specsDir = join(vaultBase, '07-Specs');
25
+ const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
25
26
 
26
27
  if (sub === 'list') {
27
28
  let files = [];
package/src/sync-defs.mjs CHANGED
@@ -5,11 +5,67 @@
5
5
  // .brain/skills/<name>/ -> <project>/.claude/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, readdirSync, statSync, writeFileSync } from 'node:fs';
8
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
9
9
  import { isAbsolute, join, resolve } from 'node:path';
10
10
 
11
+ // Managed AGENTS.md section (0.8.0): the agent-agnostic distribution channel. Codex, Amp,
12
+ // Cursor, Zed et al. read AGENTS.md — one file covers them all. Only the content between
13
+ // the markers is ours; user content around it is never touched.
14
+ const AG_START = '<!-- wendkeep:skills:start -->';
15
+ const AG_END = '<!-- wendkeep:skills:end -->';
16
+
17
+ function skillInventory(skillsSrc) {
18
+ const out = [];
19
+ let names = [];
20
+ try { names = readdirSync(skillsSrc); } catch { return out; }
21
+ for (const name of names) {
22
+ try {
23
+ if (!statSync(join(skillsSrc, name)).isDirectory()) continue;
24
+ const md = readFileSync(join(skillsSrc, name, 'SKILL.md'), 'utf8');
25
+ const desc = (md.match(/^description:\s*(.+)$/m) || [])[1] || '';
26
+ out.push({ name, description: desc.trim() });
27
+ } catch { /* skill sem SKILL.md */ }
28
+ }
29
+ return out;
30
+ }
31
+
32
+ function renderAgentsSection(skills) {
33
+ const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
34
+ return `${AG_START}
35
+ ## wendkeep — process skills & loop
36
+
37
+ This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
38
+ through its change loop: \`wendkeep change new <slug>\` → implement tasks test-first
39
+ (tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
40
+ \`wendkeep verify --deep\` + an independent read-only verification pass writing
41
+ \`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
42
+ status\` / \`spec list\` / \`sensors list\`.
43
+
44
+ Process skills (full text in \`.claude/skills/\` and the vault's \`.brain/skills/\`):
45
+ ${list}
46
+ ${AG_END}`;
47
+ }
48
+
49
+ function upsertAgentsMd(projectPath, skillsSrc) {
50
+ const skills = skillInventory(skillsSrc);
51
+ if (!skills.length) return false;
52
+ const path = join(projectPath, 'AGENTS.md');
53
+ const section = renderAgentsSection(skills);
54
+ let content = '';
55
+ try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
56
+ if (content.includes(AG_START) && content.includes(AG_END)) {
57
+ const start = content.indexOf(AG_START);
58
+ const end = content.indexOf(AG_END) + AG_END.length;
59
+ content = content.slice(0, start) + section + content.slice(end);
60
+ } else {
61
+ content = content ? `${content.trimEnd()}\n\n${section}\n` : `${section}\n`;
62
+ }
63
+ writeFileSync(path, content, 'utf8');
64
+ return true;
65
+ }
66
+
11
67
  export function syncDefs(vaultBase, projectPath) {
12
- const out = { agents: [], skills: [] };
68
+ const out = { agents: [], skills: [], agentsMd: false };
13
69
 
14
70
  const agentsSrc = join(vaultBase, '.brain', 'agents');
15
71
  if (existsSync(agentsSrc)) {
@@ -34,6 +90,9 @@ export function syncDefs(vaultBase, projectPath) {
34
90
  }
35
91
  }
36
92
 
93
+ // Agent-agnostic channel: the managed AGENTS.md section (docs/17).
94
+ out.agentsMd = upsertAgentsMd(projectPath, skillsSrc);
95
+
37
96
  return out;
38
97
  }
39
98
 
package/src/taxonomy.mjs CHANGED
@@ -25,6 +25,7 @@ export const VAULT_FOLDERS = [
25
25
  // entrypoints + price table). Mirrors the list the old setup-vault.ps1 copied.
26
26
  export const HOOK_FILES = [
27
27
  'obsidian-common.mjs',
28
+ 'locale.mjs',
28
29
  'session-start.mjs',
29
30
  'session-ensure.mjs',
30
31
  'session-stop.mjs',
@@ -11,11 +11,20 @@ import { isAbsolute, join, resolve } from 'node:path';
11
11
  const HARD_LIMIT = 25;
12
12
  const SOFT_LIMIT = 22;
13
13
 
14
- const REQUIRED_SECTIONS = [
15
- { label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
16
- { label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
17
- { label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
18
- ];
14
+ // Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
15
+ // locale pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
16
+ const SECTION_SETS = {
17
+ 'pt-BR': [
18
+ { label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
19
+ { label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
20
+ { label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
21
+ ],
22
+ en: [
23
+ { label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
24
+ { label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
25
+ { label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
26
+ ],
27
+ };
19
28
 
20
29
  // Secret patterns reject only "real" values (length floor); abstract mentions like
21
30
  // `sk_*` / `whsec_*` (trailing asterisk) are allowed.
@@ -41,9 +50,10 @@ export function validateCore(content) {
41
50
  if (lineCount > HARD_LIMIT) {
42
51
  errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
43
52
  }
44
- for (const { label, regex } of REQUIRED_SECTIONS) {
45
- if (!regex.test(text)) errors.push(`Seção obrigatória ausente: ## ${label}`);
46
- }
53
+ // Pick the locale set that matches best; require it to be complete.
54
+ const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
55
+ const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
56
+ for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
47
57
  for (const { name, regex } of SECRET_PATTERNS) {
48
58
  const m = text.match(regex);
49
59
  if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
@@ -61,7 +71,22 @@ export function validateCore(content) {
61
71
 
62
72
  // The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
63
73
  // curated hot layer exists with the right shape from day one.
64
- export function renderCoreSkeleton() {
74
+ export function renderCoreSkeleton(localeId = 'pt-BR') {
75
+ if (localeId === 'en') {
76
+ return `# CORE — curated memory core (.brain)
77
+
78
+ > RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
79
+
80
+ ## User Preferences
81
+ - (durable preferences: language, style, conventions)
82
+
83
+ ## Active Patterns
84
+ - (active patterns/architecture another agent must know)
85
+
86
+ ## Open Items
87
+ - (open items/decisions — remove when resolved)
88
+ `;
89
+ }
65
90
  return `# CORE — núcleo curado da memória (.brain)
66
91
 
67
92
  > REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
@@ -8,6 +8,9 @@
8
8
 
9
9
  export const SNIPPET_NAME = 'wendkeep-colors';
10
10
 
11
+ import { LOCALES } from '../hooks/locale.mjs';
12
+ const PT = LOCALES['pt-BR'];
13
+
11
14
  // Base palette (hex + "r, g, b" for rgba()).
12
15
  const PALETTE = {
13
16
  blue: { hex: '#2f80ed', rgb: '47, 128, 237' },
@@ -20,42 +23,49 @@ const PALETTE = {
20
23
  slate: { hex: '#64748b', rgb: '100, 116, 139' },
21
24
  };
22
25
 
23
- // File-explorer folder -> palette key (mirrors VAULT_FOLDERS in taxonomy.mjs).
24
- const FOLDER_PALETTE = [
25
- ['00-Inbox', 'amber'],
26
- ['01-Projeto', 'indigo'],
27
- ['02-Sessões', 'blue'],
28
- ['03-Linear', 'teal'],
29
- ['04-Decisões', 'violet'],
30
- ['05-Bugs', 'red'],
31
- ['06-Aprendizados', 'green'],
32
- ['07-Specs', 'teal'],
33
- ['08-Mudanças', 'amber'],
34
- ['Templates', 'slate'],
35
- ];
36
-
37
- // Note cssclass -> palette key; `folder` is reused for the graph color groups.
38
- export const NOTE_COLORS = {
39
- session: { cssClass: 'topic-session', folder: '02-Sessões', palette: 'blue' },
40
- decision: { cssClass: 'topic-decision', folder: '04-Decisões', palette: 'violet' },
41
- bug: { cssClass: 'topic-bug', folder: '05-Bugs', palette: 'red' },
42
- learning: { cssClass: 'topic-learning', folder: '06-Aprendizados', palette: 'green' },
43
- spec: { cssClass: 'topic-spec', folder: '07-Specs', palette: 'teal' },
44
- change: { cssClass: 'topic-change', folder: '08-Mudanças', palette: 'amber' },
26
+ // File-explorer folder KEY -> palette key; folder names resolve via the vault locale.
27
+ const PALETTE_BY_KEY = {
28
+ inbox: 'amber',
29
+ project: 'indigo',
30
+ sessions: 'blue',
31
+ linear: 'teal',
32
+ decisions: 'violet',
33
+ bugs: 'red',
34
+ learnings: 'green',
35
+ specs: 'teal',
36
+ changes: 'amber',
37
+ };
38
+ function folderPalette(loc) {
39
+ return [...Object.entries(PALETTE_BY_KEY).map(([k, p]) => [loc.folders[k], p]), ['Templates', 'slate']];
40
+ }
41
+
42
+ // Note cssclass -> palette + locale folder key; `folder` resolves per locale.
43
+ const NOTE_TYPES = {
44
+ session: { cssClass: 'topic-session', folderKey: 'sessions', palette: 'blue' },
45
+ decision: { cssClass: 'topic-decision', folderKey: 'decisions', palette: 'violet' },
46
+ bug: { cssClass: 'topic-bug', folderKey: 'bugs', palette: 'red' },
47
+ learning: { cssClass: 'topic-learning', folderKey: 'learnings', palette: 'green' },
48
+ spec: { cssClass: 'topic-spec', folderKey: 'specs', palette: 'teal' },
49
+ change: { cssClass: 'topic-change', folderKey: 'changes', palette: 'amber' },
45
50
  };
51
+ // Legacy pt-shaped export (folder names resolved at pt-BR) kept for compat.
52
+ export const NOTE_COLORS = Object.fromEntries(
53
+ Object.entries(NOTE_TYPES).map(([k, v]) => [k, { cssClass: v.cssClass, folder: PT.folders[v.folderKey], palette: v.palette }]),
54
+ );
46
55
 
47
- // cssclasses that get the note-accent treatment (the four note types + a home note).
48
- const TOPIC_CLASSES = [...Object.values(NOTE_COLORS).map((n) => n.cssClass), 'vault-home'];
56
+ // cssclasses that get the note-accent treatment (the note types + a home note).
57
+ const TOPIC_CLASSES = [...Object.values(NOTE_TYPES).map((n) => n.cssClass), 'vault-home'];
49
58
  const NOTE_ACCENTS = [
50
- ...Object.values(NOTE_COLORS).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
59
+ ...Object.values(NOTE_TYPES).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
51
60
  { cssClass: 'vault-home', palette: 'indigo' },
52
61
  ];
53
62
 
54
63
  const FX = '.workspace-leaf-content[data-type="file-explorer"] .nav-folder-title';
55
- const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
56
64
  const topicReading = TOPIC_CLASSES.map((c) => `.${c}`).join(', ');
57
65
 
58
- export function renderColorSnippetCss() {
66
+ export function renderColorSnippetCss(loc = PT) {
67
+ const FOLDER_PALETTE = folderPalette(loc);
68
+ const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
59
69
  const rootVars = Object.entries(PALETTE)
60
70
  .map(([k, v]) => ` --us-${k}: ${v.hex};\n --us-${k}-rgb: ${v.rgb};`)
61
71
  .join('\n');
@@ -181,9 +191,9 @@ export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
181
191
  }
182
192
 
183
193
  // Graph color groups: one per note folder, colored from the palette.
184
- export function graphColorGroups() {
185
- return Object.values(NOTE_COLORS).map((v) => ({
186
- query: `path:"${v.folder}"`,
194
+ export function graphColorGroups(loc = PT) {
195
+ return Object.values(NOTE_TYPES).map((v) => ({
196
+ query: `path:"${loc.folders[v.folderKey]}"`,
187
197
  color: { a: 1, rgb: parseInt(PALETTE[v.palette].hex.slice(1), 16) },
188
198
  }));
189
199
  }
package/src/verify.mjs CHANGED
@@ -7,6 +7,7 @@ import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.m
7
7
  import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
8
8
  import { tasksHashOf } from '../hooks/spec-core.mjs';
9
9
  import { addLesson } from '../hooks/lessons-core.mjs';
10
+ import { getLocale } from '../hooks/locale.mjs';
10
11
 
11
12
  function today() {
12
13
  const d = new Date();
@@ -28,7 +29,7 @@ export function runVerify(argv) {
28
29
  const slug = opt(argv, '--change') || activeChange(vaultBase);
29
30
  if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
30
31
 
31
- const changeDir = join(vaultBase, '08-Mudanças', slug);
32
+ const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
32
33
  let tarefas = '';
33
34
  try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
34
35
  catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }