wendkeep 0.8.1 → 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,23 @@ 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
+
7
24
  ## [0.8.1] — 2026-07-06
8
25
 
9
26
  Polish: i18n coherence + presentation.
@@ -196,6 +213,7 @@ Initial release — the capture engine, extracted from a system in daily product
196
213
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
197
214
 
198
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
199
217
  [0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
200
218
  [0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
201
219
  [0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
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,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/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
  }