wendkeep 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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.1] — 2026-07-06
8
+
9
+ Polish: i18n coherence + presentation.
10
+
11
+ ### Added
12
+ - **Locale-aware process skills + vault docs:** an `en` vault now seeds the `wk-*` skills,
13
+ the vault README, the change template, and the specs README in English (previously
14
+ Portuguese regardless of locale). Completes the `--locale en` promise.
15
+ - **`wendkeep.sensors.json` at the repo root** — the project gates itself with its own
16
+ test/check sensors (dogfooding the harness).
17
+
18
+ ### Changed
19
+ - npm `description` now describes the harness + a2 loop (was capture-only).
20
+ - CI: `actions/checkout` and `actions/setup-node` bumped to `v5` (v4 runner deprecation).
21
+ - README: the i18n "known limitation" is resolved.
22
+
23
+ ## [0.8.0] — 2026-07-05
24
+
25
+ Reach: internationalization + agent-agnostic distribution.
26
+
27
+ ### Added
28
+ - **Vault locale (i18n):** `wendkeep init --locale en` creates an English vault
29
+ (`02-Sessions`, `04-Decisions`, `08-Changes`, …, English months, English change scaffold,
30
+ English CORE skeleton, localized theme/graph groups). The locale is a vault property
31
+ (`.brain/config.json`), locked at init; absent = `pt-BR` — existing vaults are untouched
32
+ and never renamed. Parsers are **bilingual everywhere** (`Requisito|Requirement`,
33
+ `mata mutante|kill mutant`, CORE section sets), so mixed content never breaks.
34
+ - **AGENTS.md managed section:** `sync-defs`/`init` maintain a marker-delimited section in
35
+ the project's `AGENTS.md` (loop summary + skill inventory) — one file that Codex, Amp,
36
+ Cursor, Zed and any AGENTS.md-reading agent picks up. User content around it is preserved.
37
+ - **Harness contract v1.2** (`docs/14`): locale + AGENTS.md channel.
38
+
39
+ ### Deferred
40
+ - Extra mutation-report formats (mutmut/PIT) and per-agent session-hook wiring — backlog
41
+ (`docs/17`).
42
+
7
43
  ## [0.7.0] — 2026-07-05
8
44
 
9
45
  Ergonomics: the loop without hand-editing files.
@@ -160,6 +196,8 @@ Initial release — the capture engine, extracted from a system in daily product
160
196
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
161
197
 
162
198
  <!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
199
+ [0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
200
+ [0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
163
201
  [0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
164
202
  [0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
165
203
  [0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
package/README.md CHANGED
@@ -136,7 +136,7 @@ The agent's settings.json points each hook at `npx wendkeep hook …`. On `Stop`
136
136
 
137
137
  ## Known limitations (`v0.1`)
138
138
 
139
- - **Vault folder names and month labels are Portuguese** (`02-Sessões`, `04-Decisões`, …), hardcoded in the hooks. Internationalization is not done yet the taxonomy `wendkeep init` creates matches what the hooks expect on purpose.
139
+ - **Vault folder names default to Portuguese** (`02-Sessões`, `04-Decisões`, …). Pass `wendkeep init --locale en` for an English vault (`02-Sessions`, `04-Decisions`, English scaffold/skills). The locale is a vault property, locked at init; parsers are bilingual so mixed content never breaks.
140
140
  - **Search is keyword/frontmatter scoring**, not on‑device embeddings (that's on the roadmap).
141
141
  - **Transcript formats are agent‑internal** and can change between agent versions; parsing is isolated but may need updates.
142
142
  - Installer wires **Claude Code** settings + `.mcp.json`. Codex hooks run on the same scripts but are not auto‑wired yet.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.7.0",
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.",
3
+ "version": "0.8.1",
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": {
7
7
  "wendkeep": "bin/wendkeep.mjs",
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);