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 +38 -0
- package/README.md +1 -1
- package/hooks/brain-core.mjs +2 -1
- package/hooks/change-core.mjs +34 -34
- package/hooks/harness-doctor.mjs +4 -3
- package/hooks/linked-notes.mjs +8 -6
- package/hooks/locale.mjs +73 -0
- package/hooks/obsidian-common.mjs +12 -9
- package/hooks/session-ensure.mjs +1 -1
- package/hooks/session-start.mjs +1 -1
- package/hooks/session-stop.mjs +5 -3
- package/hooks/spec-core.mjs +10 -7
- package/hooks/vault-health.mjs +3 -1
- package/package.json +2 -2
- package/src/change.mjs +6 -5
- package/src/init.mjs +36 -11
- package/src/skills-seed.mjs +146 -3
- package/src/spec.mjs +2 -1
- package/src/sync-defs.mjs +61 -2
- package/src/taxonomy.mjs +1 -0
- package/src/validate-core.mjs +34 -9
- package/src/vault-readme.mjs +86 -44
- package/src/vault-theme.mjs +40 -30
- package/src/verify.mjs +2 -1
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
|
|
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/hooks/brain-core.mjs
CHANGED
|
@@ -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,
|
|
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);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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,
|
|
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,
|
|
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} — [[${
|
|
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: [[${
|
|
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
|
-
|
|
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}
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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), `---
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -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
|
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
481
|
-
const
|
|
482
|
-
const
|
|
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);
|
package/hooks/locale.mjs
ADDED
|
@@ -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
|
-
|
|
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),
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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) => {
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -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
|
|
package/hooks/session-start.mjs
CHANGED
|
@@ -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
|
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -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:
|
|
786
|
-
bugs:
|
|
787
|
-
learnings:
|
|
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)) {
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -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
|
-
|
|
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) => `###
|
|
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,
|
|
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,
|
|
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 };
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
4
|
-
"description": "
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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);
|