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 +18 -0
- package/bin/wendkeep.mjs +1 -1
- package/hooks/linked-notes.mjs +62 -38
- package/package.json +1 -1
- package/src/sensors.mjs +48 -16
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
|
|
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
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -162,7 +162,28 @@ export function extractBugDetails(tx) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
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]
|
|
188
|
-
>
|
|
189
|
-
>
|
|
208
|
+
> [!note] ${L.autoTag}
|
|
209
|
+
> ${L.autoLine(provider.label)}
|
|
210
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
190
211
|
|
|
191
|
-
##
|
|
212
|
+
## ${L.bug.symptom}
|
|
192
213
|
|
|
193
|
-
${bug.symptom ||
|
|
214
|
+
${bug.symptom || L.verify}
|
|
194
215
|
|
|
195
|
-
##
|
|
216
|
+
## ${L.bug.rootCause}
|
|
196
217
|
|
|
197
218
|
${bug.rootCause}
|
|
198
219
|
|
|
199
|
-
##
|
|
220
|
+
## ${L.bug.fix}
|
|
200
221
|
|
|
201
|
-
${markdownList(bug.fixes,
|
|
222
|
+
${markdownList(bug.fixes, L.bug.noFix)}
|
|
202
223
|
|
|
203
|
-
##
|
|
224
|
+
## ${L.bug.files}
|
|
204
225
|
|
|
205
|
-
${markdownList(bug.changedFiles.map((file) => `\`${file}\``),
|
|
226
|
+
${markdownList(bug.changedFiles.map((file) => `\`${file}\``), L.bug.seeSession)}
|
|
206
227
|
|
|
207
|
-
##
|
|
228
|
+
## ${L.bug.evidence}
|
|
208
229
|
|
|
209
|
-
${markdownList(bug.evidence,
|
|
230
|
+
${markdownList(bug.evidence, L.bug.addEvidence)}
|
|
210
231
|
|
|
211
|
-
##
|
|
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]
|
|
331
|
-
>
|
|
332
|
-
>
|
|
352
|
+
> [!note] ${L.autoTag}
|
|
353
|
+
> ${L.autoLine(provider.label)}
|
|
354
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
333
355
|
|
|
334
|
-
##
|
|
356
|
+
## ${L.dec.context}
|
|
335
357
|
|
|
336
|
-
${decision.context ||
|
|
358
|
+
${decision.context || L.complete}
|
|
337
359
|
|
|
338
|
-
##
|
|
360
|
+
## ${L.dec.decision}
|
|
339
361
|
|
|
340
362
|
${decision.detail}
|
|
341
363
|
|
|
342
|
-
##
|
|
364
|
+
## ${L.dec.consequences}
|
|
343
365
|
|
|
344
366
|
${decision.consequences}
|
|
345
367
|
|
|
346
|
-
##
|
|
368
|
+
## ${L.dec.alternatives}
|
|
347
369
|
|
|
348
|
-
${markdownList(decision.alternatives,
|
|
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
|
-
#
|
|
453
|
+
# ${L.learn.title} - ${learning.title}
|
|
431
454
|
|
|
432
|
-
> [!note]
|
|
433
|
-
>
|
|
434
|
-
>
|
|
455
|
+
> [!note] ${L.autoTag}
|
|
456
|
+
> ${L.autoLine(provider.label)}
|
|
457
|
+
> ${L.session}: ${wikilinkFromRel(sessionRel)}
|
|
435
458
|
|
|
436
|
-
##
|
|
459
|
+
## ${L.learn.context}
|
|
437
460
|
|
|
438
|
-
${learning.context ||
|
|
461
|
+
${learning.context || L.complete}
|
|
439
462
|
|
|
440
|
-
##
|
|
463
|
+
## ${L.learn.learned}
|
|
441
464
|
|
|
442
465
|
${learning.content}
|
|
443
466
|
|
|
444
|
-
##
|
|
467
|
+
## ${L.learn.future}
|
|
445
468
|
|
|
446
|
-
|
|
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
|
|
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.
|
|
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
|
|
2
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
52
|
+
|
|
53
|
+
process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list, add.\n`);
|
|
54
|
+
process.exit(2);
|
|
23
55
|
}
|