wendkeep 0.8.0 → 0.9.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,39 @@ 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.9.0] — 2026-07-06
8
+
9
+ Engineering debt: sensor editing + i18n coherence for auto-generated notes.
10
+
11
+ ### Added
12
+ - **`wendkeep sensors add <id> "<command>"`** (`--severity` / `--type` / `--report` / `--name`
13
+ / `--description`) — append a sensor to `wendkeep.sensors.json` (creates the file with
14
+ `$schema` when absent, dedups by id) instead of hand-editing JSON.
15
+ - **Locale-aware derived notes:** the auto-generated bug/decision/learning notes render their
16
+ headings + callout in the vault locale — an `en` vault no longer gets Portuguese headings.
17
+
18
+ ### Deferred (with reason)
19
+ - `migrate-locale`: renaming a populated vault breaks every wikilink to the old folder names;
20
+ needs a backlink-repair pass — its own effort, not a patch.
21
+ - Code-hash verdict freshness: a change carries no file manifest, so "the code" is undefined;
22
+ the existing `tarefas.md` hash already blocks task drift.
23
+
24
+ ## [0.8.1] — 2026-07-06
25
+
26
+ Polish: i18n coherence + presentation.
27
+
28
+ ### Added
29
+ - **Locale-aware process skills + vault docs:** an `en` vault now seeds the `wk-*` skills,
30
+ the vault README, the change template, and the specs README in English (previously
31
+ Portuguese regardless of locale). Completes the `--locale en` promise.
32
+ - **`wendkeep.sensors.json` at the repo root** — the project gates itself with its own
33
+ test/check sensors (dogfooding the harness).
34
+
35
+ ### Changed
36
+ - npm `description` now describes the harness + a2 loop (was capture-only).
37
+ - CI: `actions/checkout` and `actions/setup-node` bumped to `v5` (v4 runner deprecation).
38
+ - README: the i18n "known limitation" is resolved.
39
+
7
40
  ## [0.8.0] — 2026-07-05
8
41
 
9
42
  Reach: internationalization + agent-agnostic distribution.
@@ -180,6 +213,8 @@ Initial release — the capture engine, extracted from a system in daily product
180
213
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
181
214
 
182
215
  <!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
216
+ [0.9.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.0
217
+ [0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
183
218
  [0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
184
219
  [0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
185
220
  [0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
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.
package/bin/wendkeep.mjs CHANGED
@@ -46,7 +46,7 @@ Usage:
46
46
  wendkeep change <sub> Change lifecycle: new [--simple] | list | show | status |
47
47
  done <id> | undone <id> | diff | archive [--force].
48
48
  wendkeep spec <sub> Living specs: list | show <capability>.
49
- wendkeep sensors list Show the sensors from wendkeep.sensors.json.
49
+ wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
50
50
  wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
51
51
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
52
52
  wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
@@ -162,7 +162,28 @@ export function extractBugDetails(tx) {
162
162
  };
163
163
  }
164
164
 
165
- export function buildBugNoteContent(bug, issueRef, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(bug.rootCause)) {
165
+ // Locale labels for the auto-generated derived notes (0.9.0). Output-only the extraction
166
+ // heuristics are untouched. Default pt-BR keeps existing behaviour for every legacy caller.
167
+ const NOTE_LABELS = {
168
+ 'pt-BR': {
169
+ autoTag: 'Auto-gerada', autoLine: (p) => `Nota criada automaticamente pelo hook Stop do ${p}.`, session: 'Sessão',
170
+ verify: '_Extraído da sessão — verificar._', complete: '_Extraído da sessão — complementar._',
171
+ bug: { symptom: 'Sintoma', rootCause: 'Causa raiz', fix: 'Correção', files: 'Arquivos alterados', evidence: 'Evidência', lessons: 'Lições aprendidas', noFix: '_Nenhuma correção explícita detectada._', seeSession: '_Ver sessão vinculada._', addEvidence: '_Adicionar evidência empírica._' },
172
+ dec: { context: 'Contexto', decision: 'Decisão', consequences: 'Consequências', alternatives: 'Alternativas consideradas', noAlt: '_Nenhuma alternativa registrada automaticamente._' },
173
+ learn: { title: 'Aprendizado', context: 'Contexto', learned: 'O que aprendemos', future: 'Como aplicar no futuro', futureHint: '_Registrar como este conhecimento pode ser reutilizado._' },
174
+ },
175
+ en: {
176
+ autoTag: 'Auto-generated', autoLine: (p) => `Note created automatically by the ${p} Stop hook.`, session: 'Session',
177
+ verify: '_Extracted from the session — verify._', complete: '_Extracted from the session — complete._',
178
+ bug: { symptom: 'Symptom', rootCause: 'Root cause', fix: 'Fix', files: 'Changed files', evidence: 'Evidence', lessons: 'Lessons learned', noFix: '_No explicit fix detected._', seeSession: '_See the linked session._', addEvidence: '_Add empirical evidence._' },
179
+ dec: { context: 'Context', decision: 'Decision', consequences: 'Consequences', alternatives: 'Alternatives considered', noAlt: '_No alternative recorded automatically._' },
180
+ learn: { title: 'Learning', context: 'Context', learned: 'What we learned', future: 'How to apply in future', futureHint: '_Record how this knowledge can be reused._' },
181
+ },
182
+ };
183
+ function noteLabels(localeId) { return NOTE_LABELS[localeId] || NOTE_LABELS['pt-BR']; }
184
+
185
+ export function buildBugNoteContent(bug, issueRef, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(bug.rootCause), localeId = 'pt-BR') {
186
+ const L = noteLabels(localeId);
166
187
  const title = issueRef
167
188
  ? `${issueRef} - ${normalizeInline(bug.rootCause, 80)}`
168
189
  : normalizeInline(bug.rootCause, 80);
@@ -184,31 +205,31 @@ issue: ${yamlQuote(issueRef || '')}
184
205
 
185
206
  # Bug - ${title}
186
207
 
187
- > [!note] Auto-gerada
188
- > Nota criada automaticamente pelo hook Stop do ${provider.label}.
189
- > Sessão: ${wikilinkFromRel(sessionRel)}
208
+ > [!note] ${L.autoTag}
209
+ > ${L.autoLine(provider.label)}
210
+ > ${L.session}: ${wikilinkFromRel(sessionRel)}
190
211
 
191
- ## Sintoma
212
+ ## ${L.bug.symptom}
192
213
 
193
- ${bug.symptom || '_Extraído da sessão — verificar._'}
214
+ ${bug.symptom || L.verify}
194
215
 
195
- ## Causa raiz
216
+ ## ${L.bug.rootCause}
196
217
 
197
218
  ${bug.rootCause}
198
219
 
199
- ## Correção
220
+ ## ${L.bug.fix}
200
221
 
201
- ${markdownList(bug.fixes, '_Nenhuma correção explícita detectada._')}
222
+ ${markdownList(bug.fixes, L.bug.noFix)}
202
223
 
203
- ## Arquivos alterados
224
+ ## ${L.bug.files}
204
225
 
205
- ${markdownList(bug.changedFiles.map((file) => `\`${file}\``), '_Ver sessão vinculada._')}
226
+ ${markdownList(bug.changedFiles.map((file) => `\`${file}\``), L.bug.seeSession)}
206
227
 
207
- ## Evidência
228
+ ## ${L.bug.evidence}
208
229
 
209
- ${markdownList(bug.evidence, '_Adicionar evidência empírica._')}
230
+ ${markdownList(bug.evidence, L.bug.addEvidence)}
210
231
 
211
- ## Lições aprendidas
232
+ ## ${L.bug.lessons}
212
233
 
213
234
  ${bug.lessons}
214
235
  `;
@@ -309,7 +330,8 @@ export function extractDecisionDetails(tx) {
309
330
  };
310
331
  }
311
332
 
312
- export function buildDecisionNoteContent(decision, adrNum, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(decision.title)) {
333
+ export function buildDecisionNoteContent(decision, adrNum, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(decision.title), localeId = 'pt-BR') {
334
+ const L = noteLabels(localeId);
313
335
  const adrId = `ADR-${String(adrNum).padStart(3, '0')}`;
314
336
  return `---
315
337
  type: decision
@@ -327,25 +349,25 @@ superseded_by: ""
327
349
 
328
350
  # ${adrId} - ${decision.title}
329
351
 
330
- > [!note] Auto-gerada
331
- > Nota criada automaticamente pelo hook Stop do ${provider.label}.
332
- > Sessão: ${wikilinkFromRel(sessionRel)}
352
+ > [!note] ${L.autoTag}
353
+ > ${L.autoLine(provider.label)}
354
+ > ${L.session}: ${wikilinkFromRel(sessionRel)}
333
355
 
334
- ## Contexto
356
+ ## ${L.dec.context}
335
357
 
336
- ${decision.context || '_Extraído da sessão — complementar._'}
358
+ ${decision.context || L.complete}
337
359
 
338
- ## Decisão
360
+ ## ${L.dec.decision}
339
361
 
340
362
  ${decision.detail}
341
363
 
342
- ## Consequências
364
+ ## ${L.dec.consequences}
343
365
 
344
366
  ${decision.consequences}
345
367
 
346
- ## Alternativas consideradas
368
+ ## ${L.dec.alternatives}
347
369
 
348
- ${markdownList(decision.alternatives, '_Nenhuma alternativa registrada automaticamente._')}
370
+ ${markdownList(decision.alternatives, L.dec.noAlt)}
349
371
  `;
350
372
  }
351
373
 
@@ -413,7 +435,8 @@ export function extractLearningDetails(tx, bugDetails) {
413
435
  return learnings.length ? learnings.slice(0, 5) : null;
414
436
  }
415
437
 
416
- export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title)) {
438
+ export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title), localeId = 'pt-BR') {
439
+ const L = noteLabels(localeId);
417
440
  return `---
418
441
  type: learning
419
442
  date: ${dateStr}
@@ -427,23 +450,23 @@ tags:
427
450
  ${yamlTags(learning.tags.map((tag) => (tag === 'codex' ? provider.tag : tag)))}
428
451
  ---
429
452
 
430
- # Aprendizado - ${learning.title}
453
+ # ${L.learn.title} - ${learning.title}
431
454
 
432
- > [!note] Auto-gerada
433
- > Nota criada automaticamente pelo hook Stop do ${provider.label}.
434
- > Sessão: ${wikilinkFromRel(sessionRel)}
455
+ > [!note] ${L.autoTag}
456
+ > ${L.autoLine(provider.label)}
457
+ > ${L.session}: ${wikilinkFromRel(sessionRel)}
435
458
 
436
- ## Contexto
459
+ ## ${L.learn.context}
437
460
 
438
- ${learning.context || '_Extraído da sessão — complementar._'}
461
+ ${learning.context || L.complete}
439
462
 
440
- ## O que aprendemos
463
+ ## ${L.learn.learned}
441
464
 
442
465
  ${learning.content}
443
466
 
444
- ## Como aplicar no futuro
467
+ ## ${L.learn.future}
445
468
 
446
- _Registrar como este conhecimento pode ser reutilizado._
469
+ ${L.learn.futureHint}
447
470
  `;
448
471
  }
449
472
 
@@ -478,7 +501,8 @@ function alreadyHasKey(keys, candidate) {
478
501
  export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options = {}) {
479
502
  const linked = { decisions: [], bugs: [], learnings: [] };
480
503
  const provider = providerMeta(options.provider);
481
- const locF = getLocale(vaultBase).folders;
504
+ const loc = getLocale(vaultBase);
505
+ const locF = loc.folders;
482
506
  const bugsDir = join(vaultBase, monthFolderRelFromDateStr(locF.bugs, dateStr, vaultBase));
483
507
  const decisionsDir = join(vaultBase, monthFolderRelFromDateStr(locF.decisions, dateStr, vaultBase));
484
508
  const learningsDir = join(vaultBase, monthFolderRelFromDateStr(locF.learnings, dateStr, vaultBase));
@@ -497,7 +521,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
497
521
  const causeSlug = slugify(bugDetails.rootCause.slice(0, 40), 'bug');
498
522
  const fileName = issueRef ? `${issueRef}-${causeSlug}.md` : `${dateStr}-bug-${causeSlug}.md`;
499
523
  const filePath = join(bugsDir, fileName);
500
- if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey), 'utf-8');
524
+ if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id), 'utf-8');
501
525
  linked.bugs.push(toVaultRelative(vaultBase, filePath));
502
526
  existingKeys.bugs.push(bugKey);
503
527
  }
@@ -513,7 +537,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
513
537
  const filePath = join(decisionsDir, fileName);
514
538
  if (!existsSync(filePath)) {
515
539
  const adrNum = Number(fileName.match(/^ADR-(\d+)/i)?.[1]) || getNextAdrNumber(vaultBase);
516
- writeFileSync(filePath, buildDecisionNoteContent(decisionDetails, adrNum, dateStr, sessionRel, provider, decisionKey), 'utf-8');
540
+ writeFileSync(filePath, buildDecisionNoteContent(decisionDetails, adrNum, dateStr, sessionRel, provider, decisionKey, loc.id), 'utf-8');
517
541
  }
518
542
  linked.decisions.push(toVaultRelative(vaultBase, filePath));
519
543
  existingKeys.decisions.push(decisionKey);
@@ -528,7 +552,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
528
552
  const learningSlug = slugify(learning.title.slice(0, 40), 'aprendizado');
529
553
  const fileName = `${dateStr}-${learningSlug}.md`;
530
554
  const filePath = join(learningsDir, fileName);
531
- if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey), 'utf-8');
555
+ if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id), 'utf-8');
532
556
  linked.learnings.push(toVaultRelative(vaultBase, filePath));
533
557
  existingKeys.learnings.push(learningKey);
534
558
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.8.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.9.0",
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/init.mjs CHANGED
@@ -282,7 +282,7 @@ export async function runInit(argv) {
282
282
  const readmePath = join(vaultPath, 'README.md');
283
283
  let readmeNote = '';
284
284
  if (!existsSync(readmePath)) {
285
- writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
285
+ writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp, locale: loc.id }), 'utf8');
286
286
  readmeNote = ', README.md created';
287
287
  }
288
288
  // Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
@@ -297,12 +297,21 @@ export async function runInit(argv) {
297
297
  // Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
298
298
  // truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
299
299
  seedDefinitions(brainDir);
300
- seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
300
+ seedWkSkills(brainDir, loc.id); // Pilar A: native process skills, in the vault locale.
301
301
  // Seed the change/spec layer starters (Pilar B) — non-destructive.
302
+ const en = loc.id === 'en';
302
303
  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');
304
+ if (!existsSync(specsReadme)) {
305
+ writeFileSync(specsReadme, en
306
+ ? `# Specs — living contract\n\nThe project's capabilities (requirements/scenarios). Changes in \`${loc.folders.changes}/\` promote deltas here on \`wendkeep change archive\`.\n`
307
+ : `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
308
+ }
304
309
  const changeTpl = join(vaultPath, 'Templates', 'Change.md');
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');
310
+ if (!existsSync(changeTpl)) {
311
+ writeFileSync(changeTpl, en
312
+ ? '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Why\n\n## What changes\n'
313
+ : '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
314
+ }
306
315
  // Seed the native sensor config (Pilar C) at project root — non-destructive.
307
316
  const sensorsFile = join(projectPath, 'wendkeep.sensors.json');
308
317
  if (!existsSync(sensorsFile)) {
package/src/sensors.mjs CHANGED
@@ -1,23 +1,55 @@
1
- // `wendkeep sensors list`read-only view over wendkeep.sensors.json (0.7.0).
2
- import { resolve } from 'node:path';
1
+ // `wendkeep sensors <list|add>` — view/edit wendkeep.sensors.json (0.7.0 / 0.9.0).
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
3
4
  import { loadSensors } from '../hooks/sensors-core.mjs';
4
5
 
6
+ const SCHEMA = 'https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json';
7
+
8
+ function opt(argv, name) {
9
+ const i = argv.indexOf(name);
10
+ if (i >= 0) return argv[i + 1];
11
+ const eq = argv.find((a) => a.startsWith(`${name}=`));
12
+ return eq ? eq.slice(name.length + 1) : undefined;
13
+ }
14
+
15
+ function resolveProject(rest) {
16
+ return resolve(opt(rest, '--project') || process.cwd());
17
+ }
18
+
5
19
  export function runSensors(argv) {
6
20
  const [sub, ...rest] = argv;
7
- if (sub !== 'list') {
8
- process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list.\n`);
9
- process.exit(2);
10
- }
11
- let project;
12
- for (let i = 0; i < rest.length; i += 1) {
13
- if (rest[i] === '--project') project = rest[++i];
14
- else if (rest[i].startsWith('--project=')) project = rest[i].slice(10);
21
+
22
+ if (sub === 'list') {
23
+ const sensors = loadSensors(resolveProject(rest));
24
+ if (!sensors.length) { process.stdout.write('sensors: (nenhum — crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
25
+ for (const s of sensors) process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
26
+ process.exit(0);
15
27
  }
16
- const projectRoot = resolve(project || process.cwd());
17
- const sensors = loadSensors(projectRoot);
18
- if (!sensors.length) { process.stdout.write('sensors: (nenhum crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
19
- for (const s of sensors) {
20
- process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
28
+
29
+ if (sub === 'add') {
30
+ const flags = new Set(['--severity', '--type', '--report', '--name', '--description', '--project']);
31
+ const positional = rest.filter((a, i) => !a.startsWith('--') && !flags.has(rest[i - 1]));
32
+ const [id, command] = positional;
33
+ if (!id || !command) { process.stderr.write('wendkeep sensors add: precisa <id> "<command>"\n'); process.exit(2); }
34
+ const projectRoot = resolveProject(rest);
35
+ const path = join(projectRoot, 'wendkeep.sensors.json');
36
+ let cfg = { $schema: SCHEMA, version: 1, source: 'manual', sensors: [] };
37
+ if (existsSync(path)) {
38
+ try { cfg = JSON.parse(readFileSync(path, 'utf8')); } catch { process.stderr.write('wendkeep sensors add: wendkeep.sensors.json inválido\n'); process.exit(2); }
39
+ if (!Array.isArray(cfg.sensors)) cfg.sensors = [];
40
+ if (!cfg.$schema) cfg.$schema = SCHEMA;
41
+ }
42
+ if (cfg.sensors.some((s) => s.id === id)) { process.stderr.write(`wendkeep sensors add: sensor "${id}" já existe\n`); process.exit(2); }
43
+ const severity = opt(rest, '--severity') === 'warning' ? 'warning' : 'critical';
44
+ const type = opt(rest, '--type') === 'mutation' ? 'mutation' : 'command';
45
+ const sensor = { id, name: opt(rest, '--name') || id, description: opt(rest, '--description') || command, severity, type, command };
46
+ if (type === 'mutation') sensor.report = opt(rest, '--report') || 'reports/mutation/mutation.json';
47
+ cfg.sensors.push(sensor);
48
+ writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
49
+ process.stdout.write(`sensor added: ${id} (${type} · ${severity})\n`);
50
+ process.exit(0);
21
51
  }
22
- process.exit(0);
52
+
53
+ process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list, add.\n`);
54
+ process.exit(2);
23
55
  }
@@ -203,7 +203,135 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
203
203
  - O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
204
204
  `;
205
205
 
206
- export const WK_SKILLS = [
206
+ const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
207
+
208
+ Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
209
+ (sensors) together, wikilinked in the Obsidian graph.
210
+
211
+ ## Steps
212
+
213
+ 1. **Explore** — understand the problem before proposing.
214
+ 2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
215
+ (proposta/design/tarefas + a \`specs/\` delta). The change becomes *active* and is
216
+ injected at the next SessionStart.
217
+ 3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
218
+ \`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
219
+ 4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
220
+ the verification package.
221
+ 5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
222
+ A trivial change (no \`[req:]\`) gets an auto verdict.
223
+ 6. **Archive** — \`wendkeep change archive <slug>\`. The gate needs green sensors AND a
224
+ verdict AND no open tasks. It promotes the delta into \`07-Specs\` and mints an ADR.
225
+
226
+ ## Rules
227
+ - One active change at a time. Finish (archive) before starting another.
228
+ - No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
229
+ - The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
230
+ `;
231
+
232
+ const TDD_EN = `# TDD — Red, Green, Refactor (tests that discriminate)
233
+
234
+ Never write production code without a red test asking for it — and never write a test that
235
+ would pass under the wrong implementation.
236
+
237
+ ## The cycle
238
+ 1. **Red** — the smallest test that fails for the missing behaviour.
239
+ 2. **See it fail** for the right reason (not an import/typo).
240
+ 3. **Green** — the minimal code to pass.
241
+ 4. **Refactor** with the greens protecting you.
242
+
243
+ ## Derive from the spec, not the code
244
+ Write assertions from the requirement's acceptance criteria (\`07-Specs\`), not by reading the
245
+ implementation. Reading the code to write the test = it only confirms what the code already does.
246
+
247
+ ## Non-shallow litmus
248
+ Reject an assertion that would pass under a wrong implementation. Assert a **value or persisted
249
+ state**, never "the mock was called". "Tested later" is rejected.
250
+
251
+ ## Adequacy (necessary and sufficient)
252
+ Every acceptance criterion has an assertion with \`file:line\` evidence; every test traces to a
253
+ requirement (\`[req:ID]\`); covers what the spec asks, nothing more.
254
+
255
+ ## Learn the project
256
+ Sample 5–10 existing tests for style/location/framework/depth (treat that depth as a floor).
257
+ Read \`AGENTS.md\` / \`.cursor/rules\` / CI configs for declared standards.
258
+ `;
259
+
260
+ const DEBUGGING_EN = `# Systematic debugging
261
+
262
+ Use it when something fails or behaves wrong. One hypothesis at a time — no shotgun changes.
263
+
264
+ 1. **Reproduce** — a minimal deterministic case. Without it you can't know you fixed anything.
265
+ 2. **One hypothesis** — the most likely cause, specific enough to test.
266
+ 3. **Isolate** — confirm or kill the hypothesis BEFORE fixing (bisect, log, comment half). Prove
267
+ where it is, not where you think it is.
268
+ 4. **Fix the root**, not the symptom.
269
+ 5. **Verify** — the repro is gone AND the suite stays green. No regression.
270
+
271
+ Changed several things and it "worked"? You don't know what fixed it — revert, isolate one
272
+ variable. Read the whole error + stack — the decisive line is usually there.
273
+ `;
274
+
275
+ const BRAINSTORMING_EN = `# Brainstorming — idea into design
276
+
277
+ Use it when the idea is still vague. Turn it into an approved design BEFORE writing code.
278
+
279
+ 1. **Explore context** — files, docs, relevant history.
280
+ 2. **One question at a time** — purpose, constraints, success criteria. Prefer multiple choice.
281
+ 3. **Propose 2–3 approaches** with trade-offs and your recommendation.
282
+ 4. **Present the design in sections**, confirm each.
283
+
284
+ ## Closure gate — no dangling ambiguity
285
+ Resolve every gray area: decide with the user, or log a **signed-off assumption** ("assuming X
286
+ because Y — correct me if wrong"). A declined gray area is recorded, not silently dropped.
287
+
288
+ ## Out-of-scope table
289
+ List explicitly what the change does **not** do. Undeclared scope becomes creep.
290
+
291
+ ## Hard gate
292
+ No code / scaffold / implementation action until a design is presented and approved. Then go to
293
+ **wk-planning**.
294
+ `;
295
+
296
+ const PLANNING_EN = `# Planning — design into a task plan
297
+
298
+ Use it after an approved design. Produce a plan an engineer with no project context can execute.
299
+
300
+ ## Before tasks
301
+ Map the files: what to create/modify and each one's responsibility. Files that change together
302
+ live together; one file, one responsibility.
303
+
304
+ ## Bite-sized tasks (TDD)
305
+ Each task ends in an independently testable deliverable. Each step is a 2–5 min action:
306
+ write the failing test (show code) → run and see it fail (exact command + expected) → minimal
307
+ implementation (show code) → run and see it pass → checkpoint: suite green.
308
+
309
+ ## Rules
310
+ - Exact file paths, always. Real code in each step — no "TODO", "handle errors appropriately",
311
+ "similar to Task N". DRY, YAGNI. Consistent names/signatures across tasks.
312
+ `;
313
+
314
+ const VERIFY_EN = `# Independent verification — the fresh pass
315
+
316
+ Use it in the *verify deep* step, after \`wendkeep verify --deep\`. You are the **verifier**, not
317
+ the author — even if you wrote the code, enter as if you'd never seen it. Fresh context, read-only.
318
+
319
+ ## What to do
320
+ 1. Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence).
321
+ 2. **Re-derive coverage from \`07-Specs\`** — for each \`[req:ID]\`, check the requirement's behaviour
322
+ is covered by a test that discriminates (wouldn't pass under a wrong impl). \`file:line\` evidence.
323
+ 3. Spec-anchored outcome check: does the observable result match the acceptance criterion?
324
+ 4. Write \`08-Changes/<slug>/verdict.json\`:
325
+ \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "notes": [] }\`.
326
+
327
+ ## Rules
328
+ - **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
329
+ - \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
330
+ - Don't fix here — a gap becomes a fix task; re-verify after.
331
+ - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
332
+ `;
333
+
334
+ const WK_SKILLS_PT = [
207
335
  skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
208
336
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
209
337
  skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
@@ -212,10 +340,25 @@ export const WK_SKILLS = [
212
340
  skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
213
341
  ];
214
342
 
343
+ const WK_SKILLS_EN = [
344
+ skill('wk-workflow', 'Use when starting any non-trivial change — orchestrates the a2 loop (explore, propose, apply, verify, archive) over the wendkeep commands.', WORKFLOW_EN),
345
+ skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
346
+ skill('wk-debugging', 'Use when something fails or breaks — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
347
+ skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN),
348
+ skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN),
349
+ skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN),
350
+ ];
351
+
352
+ // Skill set for a locale. WK_SKILLS stays the pt-BR set for back-compat.
353
+ export function wkSkills(localeId = 'pt-BR') {
354
+ return localeId === 'en' ? WK_SKILLS_EN : WK_SKILLS_PT;
355
+ }
356
+ export const WK_SKILLS = WK_SKILLS_PT;
357
+
215
358
  // Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
216
- export function seedWkSkills(brainDir) {
359
+ export function seedWkSkills(brainDir, localeId = 'pt-BR') {
217
360
  const created = [];
218
- for (const s of WK_SKILLS) {
361
+ for (const s of wkSkills(localeId)) {
219
362
  const dir = join(brainDir, 'skills', s.name);
220
363
  mkdirSync(dir, { recursive: true });
221
364
  const f = join(dir, 'SKILL.md');
@@ -1,46 +1,90 @@
1
1
  // Renders a neutral, project-templated README for a freshly created wendkeep vault.
2
- // The structure table is derived from VAULT_FOLDERS so it can never drift from the
3
- // folders `wendkeep init` actually creates.
4
- import { VAULT_FOLDERS } from './taxonomy.mjs';
5
-
6
- const FOLDER_DESCRIPTIONS = {
7
- '00-Inbox': 'Notas rápidas, captura sem classificação',
8
- '01-Projeto': 'Arquitetura, padrões, memória do projeto',
9
- '02-Sessões': 'Uma nota por sessão de trabalho com o agente IA',
10
- '03-Linear': 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
11
- '04-Decisões': 'Architecture Decision Records (ADRs)',
12
- '05-Bugs': 'Bugs resolvidos com causa raiz documentada',
13
- '06-Aprendizados': 'Lições extraídas das sessões',
14
- Templates: 'Templates para novas notas',
15
- '.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
2
+ // Locale-aware (0.8.1): the structure table + prose follow the vault locale; folder names
3
+ // come from the locale so they can never drift from what `wendkeep init` creates.
4
+ import { LOCALES, vaultFolders } from '../hooks/locale.mjs';
5
+
6
+ // Folder descriptions keyed by locale folder KEY (not literal name), per locale.
7
+ const DESC = {
8
+ 'pt-BR': {
9
+ inbox: 'Notas rápidas, captura sem classificação',
10
+ project: 'Arquitetura, padrões, memória do projeto',
11
+ sessions: 'Uma nota por sessão de trabalho com o agente IA',
12
+ linear: 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
13
+ decisions: 'Architecture Decision Records (ADRs)',
14
+ bugs: 'Bugs resolvidos com causa raiz documentada',
15
+ learnings: 'Lições extraídas das sessões',
16
+ specs: 'Contrato vivo: capacidades do projeto (requisitos)',
17
+ changes: 'Mudanças em andamento (proposta/design/tarefas/spec-delta)',
18
+ Templates: 'Templates para novas notas',
19
+ '.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
20
+ },
21
+ en: {
22
+ inbox: 'Quick notes, unclassified capture',
23
+ project: 'Architecture, patterns, project memory',
24
+ sessions: 'One note per AI-agent work session',
25
+ linear: 'Mirror of issues/tasks from your tracker (Linear, Jira, GitHub Issues…)',
26
+ decisions: 'Architecture Decision Records (ADRs)',
27
+ bugs: 'Fixed bugs with documented root cause',
28
+ learnings: 'Lessons extracted from sessions',
29
+ specs: 'Living contract: the project capabilities (requirements)',
30
+ changes: 'Changes in flight (proposal/design/tasks/spec-delta)',
31
+ Templates: 'Templates for new notes',
32
+ '.brain': 'Canonical memory: CORE.md (curated) + DIGEST.md (auto), injected at SessionStart',
33
+ },
16
34
  };
17
35
 
18
- function structureTable() {
19
- const rows = VAULT_FOLDERS.map((f) => {
20
- const desc = FOLDER_DESCRIPTIONS[f] || 'Notas do projeto';
21
- return `| \`${f}/\` | ${desc} |`;
22
- });
23
- return ['| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
36
+ // folder literal -> key, for description lookup.
37
+ function keyForFolder(loc, folder) {
38
+ const entry = Object.entries(loc.folders).find(([, name]) => name === folder);
39
+ return entry ? entry[0] : folder; // Templates / .brain fall through as themselves
24
40
  }
25
41
 
26
- export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
27
- const name = projectName || 'projeto';
28
- const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
42
+ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, locale = 'pt-BR' }) {
43
+ const loc = LOCALES[locale] || LOCALES['pt-BR'];
44
+ const en = loc.id === 'en';
45
+ const desc = DESC[loc.id] || DESC['pt-BR'];
46
+ const name = projectName || (en ? 'project' : 'projeto');
47
+
48
+ const rows = vaultFolders(loc).map((f) => `| \`${f}/\` | ${desc[keyForFolder(loc, f)] || (en ? 'Project notes' : 'Notas do projeto')} |`);
49
+ const table = [en ? '| Folder | Contents |' : '| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
50
+
51
+ if (en) {
52
+ const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
53
+ const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
54
+ if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
55
+ access.push('- **Hooks:** `settings.json` calls `npx wendkeep hook <name>`; the vault is located via `OBSIDIAN_VAULT_PATH` (also written by `wendkeep init`).');
56
+ return `# Obsidian vault — ${name}
57
+
58
+ > Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
59
+ > sessions (**Claude Code**, **Codex**) via session hooks${mcpIntro}.
60
+
61
+ ## Structure
62
+
63
+ ${table}
64
+
65
+ ## Access
29
66
 
30
- const access = [
31
- `- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``,
32
- ];
33
- if (withMcp) {
34
- access.push(
35
- '- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault\n' +
36
- ' pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.',
37
- );
67
+ ${access.join('\n')}
68
+
69
+ ## Rules
70
+
71
+ 1. Every relevant work session produces a note in \`${loc.folders.sessions}/\` (automatic, via hooks).
72
+ 2. Architecture decisions become ADRs in \`${loc.folders.decisions}/\`.
73
+ 3. Bugs with a root cause go to \`${loc.folders.bugs}/\`; lessons to \`${loc.folders.learnings}/\`.
74
+ 4. Canonical memory: \`.brain/CORE.md\` (curated) + \`.brain/DIGEST.md\` (auto) — injected at SessionStart.
75
+ 5. Tags in YAML frontmatter, not inline. Wikilinks \`[[note]]\` to connect notes.
76
+
77
+ ## Notes
78
+
79
+ - **Locale:** this vault is \`en\` (folder names in English). Set at \`wendkeep init --locale\`; a vault is never renamed after.
80
+ - **Versioning:** session notes may contain transcript excerpts — review before committing this vault to a shared repo.
81
+ `;
38
82
  }
39
- access.push(
40
- '- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado\n' +
41
- ' via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).',
42
- );
43
83
 
84
+ const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
85
+ const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
86
+ if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
87
+ access.push('- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).');
44
88
  return `# Vault Obsidian — ${name}
45
89
 
46
90
  > Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
@@ -49,7 +93,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
49
93
 
50
94
  ## Estrutura
51
95
 
52
- ${structureTable()}
96
+ ${table}
53
97
 
54
98
  ## Acesso
55
99
 
@@ -57,17 +101,15 @@ ${access.join('\n')}
57
101
 
58
102
  ## Regras
59
103
 
60
- 1. Toda sessão de trabalho relevante gera nota em \`02-Sessões/\` (automático, pelos hooks).
61
- 2. Decisões de arquitetura viram ADR em \`04-Decisões/\`.
62
- 3. Bugs com causa raiz vão para \`05-Bugs/\`; lições para \`06-Aprendizados/\`.
104
+ 1. Toda sessão de trabalho relevante gera nota em \`${loc.folders.sessions}/\` (automático, pelos hooks).
105
+ 2. Decisões de arquitetura viram ADR em \`${loc.folders.decisions}/\`.
106
+ 3. Bugs com causa raiz vão para \`${loc.folders.bugs}/\`; lições para \`${loc.folders.learnings}/\`.
63
107
  4. Memória canônica: \`.brain/CORE.md\` (curado) + \`.brain/DIGEST.md\` (auto) — injetados no SessionStart.
64
- 5. Tags em frontmatter YAML, não inline.
65
- 6. Wikilinks \`[[nota]]\` para conectar notas entre si.
108
+ 5. Tags em frontmatter YAML, não inline. Wikilinks \`[[nota]]\` para conectar notas.
66
109
 
67
110
  ## Notas
68
111
 
69
- - **Idioma das pastas:** os nomes são PT-BR (limitação conhecida; i18n no roadmap do wendkeep).
70
- - **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie
71
- antes de commitar este vault em um repositório compartilhado.
112
+ - **Locale:** este vault é \`pt-BR\` (nomes de pasta em português). Definido em \`wendkeep init --locale\`; o vault nunca é renomeado depois.
113
+ - **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie antes de commitar este vault em um repositório compartilhado.
72
114
  `;
73
115
  }