wendkeep 0.7.0 → 0.8.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 +21 -0
- 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 +1 -1
- package/src/change.mjs +6 -5
- package/src/init.mjs +24 -8
- 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-theme.mjs +40 -30
- package/src/verify.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.0] — 2026-07-05
|
|
8
|
+
|
|
9
|
+
Reach: internationalization + agent-agnostic distribution.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Vault locale (i18n):** `wendkeep init --locale en` creates an English vault
|
|
13
|
+
(`02-Sessions`, `04-Decisions`, `08-Changes`, …, English months, English change scaffold,
|
|
14
|
+
English CORE skeleton, localized theme/graph groups). The locale is a vault property
|
|
15
|
+
(`.brain/config.json`), locked at init; absent = `pt-BR` — existing vaults are untouched
|
|
16
|
+
and never renamed. Parsers are **bilingual everywhere** (`Requisito|Requirement`,
|
|
17
|
+
`mata mutante|kill mutant`, CORE section sets), so mixed content never breaks.
|
|
18
|
+
- **AGENTS.md managed section:** `sync-defs`/`init` maintain a marker-delimited section in
|
|
19
|
+
the project's `AGENTS.md` (loop summary + skill inventory) — one file that Codex, Amp,
|
|
20
|
+
Cursor, Zed and any AGENTS.md-reading agent picks up. User content around it is preserved.
|
|
21
|
+
- **Harness contract v1.2** (`docs/14`): locale + AGENTS.md channel.
|
|
22
|
+
|
|
23
|
+
### Deferred
|
|
24
|
+
- Extra mutation-report formats (mutmut/PIT) and per-agent session-hook wiring — backlog
|
|
25
|
+
(`docs/17`).
|
|
26
|
+
|
|
7
27
|
## [0.7.0] — 2026-07-05
|
|
8
28
|
|
|
9
29
|
Ergonomics: the loop without hand-editing files.
|
|
@@ -160,6 +180,7 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
160
180
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
161
181
|
|
|
162
182
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
183
|
+
[0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
|
|
163
184
|
[0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
|
|
164
185
|
[0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
|
|
165
186
|
[0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
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);
|
package/src/init.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
34
34
|
import { seedDefinitions, syncDefs } from './sync-defs.mjs';
|
|
35
35
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
36
|
+
import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
|
|
36
37
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
37
38
|
|
|
38
39
|
function parseArgs(argv) {
|
|
@@ -40,6 +41,8 @@ function parseArgs(argv) {
|
|
|
40
41
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
42
|
const a = argv[i];
|
|
42
43
|
if (a === '--vault') args.vault = argv[++i];
|
|
44
|
+
else if (a === '--locale') args.locale = argv[++i];
|
|
45
|
+
else if (a.startsWith('--locale=')) args.locale = a.slice(9);
|
|
43
46
|
else if (a === '--project') args.project = argv[++i];
|
|
44
47
|
else if (a === '--no-mcp') args.mcp = false;
|
|
45
48
|
else if (a === '--yes' || a === '-y') args.yes = true;
|
|
@@ -162,10 +165,11 @@ function runCavemanInstaller(log) {
|
|
|
162
165
|
// enable it (non-destructive), and add graph color groups. Returns a short note.
|
|
163
166
|
// Unparseable user JSON is left untouched (we only merge into valid/absent files).
|
|
164
167
|
function installVaultColors(vaultPath) {
|
|
168
|
+
const loc = getLocale(vaultPath);
|
|
165
169
|
const obsidianDir = join(vaultPath, '.obsidian');
|
|
166
170
|
const snippetsDir = join(obsidianDir, 'snippets');
|
|
167
171
|
mkdirSync(snippetsDir, { recursive: true });
|
|
168
|
-
writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(), 'utf8');
|
|
172
|
+
writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(loc), 'utf8');
|
|
169
173
|
|
|
170
174
|
let enabled = false;
|
|
171
175
|
const appPath = join(obsidianDir, 'appearance.json');
|
|
@@ -179,9 +183,9 @@ function installVaultColors(vaultPath) {
|
|
|
179
183
|
const graphPath = join(obsidianDir, 'graph.json');
|
|
180
184
|
const graphRead = readJsonSafe(graphPath);
|
|
181
185
|
if (graphRead.ok) {
|
|
182
|
-
const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups());
|
|
186
|
+
const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups(loc));
|
|
183
187
|
writeJson(graphPath, merged);
|
|
184
|
-
groups = graphColorGroups().length;
|
|
188
|
+
groups = graphColorGroups(loc).length;
|
|
185
189
|
}
|
|
186
190
|
return `snippet ${SNIPPET_NAME}.css${enabled ? ' (enabled)' : ' (enable by hand: appearance.json unreadable)'} + ${groups} graph group(s)`;
|
|
187
191
|
}
|
|
@@ -253,9 +257,21 @@ export async function runInit(argv) {
|
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
260
|
+
// Locale (0.8.0): a vault property, locked at init. Written BEFORE folder creation so
|
|
261
|
+
// the folder names follow it. Invalid/absent = pt-BR (backward compat).
|
|
256
262
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
263
|
+
const localeId = args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE;
|
|
264
|
+
if (args.locale && !LOCALES[args.locale]) {
|
|
265
|
+
log(` ! locale desconhecido "${args.locale}" — usando ${DEFAULT_LOCALE} (opções: ${Object.keys(LOCALES).join(', ')})`);
|
|
266
|
+
}
|
|
267
|
+
mkdirSync(join(vaultPath, '.brain'), { recursive: true });
|
|
268
|
+
const configPath = join(vaultPath, '.brain', 'config.json');
|
|
269
|
+
if (!existsSync(configPath)) writeFileSync(configPath, `${JSON.stringify({ locale: localeId }, null, 2)}\n`, 'utf8');
|
|
270
|
+
clearLocaleCache();
|
|
271
|
+
const loc = getLocale(vaultPath);
|
|
272
|
+
const folders = vaultFolders(loc);
|
|
257
273
|
let created = 0;
|
|
258
|
-
for (const f of
|
|
274
|
+
for (const f of folders) {
|
|
259
275
|
const p = join(vaultPath, f);
|
|
260
276
|
if (!existsSync(p)) {
|
|
261
277
|
mkdirSync(p, { recursive: true });
|
|
@@ -275,7 +291,7 @@ export async function runInit(argv) {
|
|
|
275
291
|
const brainDir = join(vaultPath, '.brain');
|
|
276
292
|
mkdirSync(brainDir, { recursive: true });
|
|
277
293
|
const corePath = join(brainDir, 'CORE.md');
|
|
278
|
-
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
|
|
294
|
+
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(loc.id), 'utf8');
|
|
279
295
|
const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
|
|
280
296
|
if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
|
|
281
297
|
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
@@ -283,8 +299,8 @@ export async function runInit(argv) {
|
|
|
283
299
|
seedDefinitions(brainDir);
|
|
284
300
|
seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
|
|
285
301
|
// Seed the change/spec layer starters (Pilar B) — non-destructive.
|
|
286
|
-
const specsReadme = join(vaultPath,
|
|
287
|
-
if (!existsSync(specsReadme)) writeFileSync(specsReadme,
|
|
302
|
+
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');
|
|
288
304
|
const changeTpl = join(vaultPath, 'Templates', 'Change.md');
|
|
289
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');
|
|
290
306
|
// Seed the native sensor config (Pilar C) at project root — non-destructive.
|
|
@@ -294,7 +310,7 @@ export async function runInit(argv) {
|
|
|
294
310
|
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
295
311
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
296
312
|
}
|
|
297
|
-
log(` [1/4] vault taxonomy: ${
|
|
313
|
+
log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded`);
|
|
298
314
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
299
315
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
300
316
|
const synced = syncDefs(vaultPath, projectPath);
|
package/src/spec.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { parseRequirements } from '../hooks/spec-core.mjs';
|
|
5
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
5
6
|
|
|
6
7
|
function resolveVault(argv) {
|
|
7
8
|
let vault;
|
|
@@ -21,7 +22,7 @@ function resolveVault(argv) {
|
|
|
21
22
|
export function runSpec(argv) {
|
|
22
23
|
const [sub, ...rest] = argv;
|
|
23
24
|
const vaultBase = resolveVault(rest);
|
|
24
|
-
const specsDir = join(vaultBase,
|
|
25
|
+
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
25
26
|
|
|
26
27
|
if (sub === 'list') {
|
|
27
28
|
let files = [];
|
package/src/sync-defs.mjs
CHANGED
|
@@ -5,11 +5,67 @@
|
|
|
5
5
|
// .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
|
|
6
6
|
// .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
|
|
7
7
|
// cross-platform robustness.
|
|
8
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
|
|
11
|
+
// Managed AGENTS.md section (0.8.0): the agent-agnostic distribution channel. Codex, Amp,
|
|
12
|
+
// Cursor, Zed et al. read AGENTS.md — one file covers them all. Only the content between
|
|
13
|
+
// the markers is ours; user content around it is never touched.
|
|
14
|
+
const AG_START = '<!-- wendkeep:skills:start -->';
|
|
15
|
+
const AG_END = '<!-- wendkeep:skills:end -->';
|
|
16
|
+
|
|
17
|
+
function skillInventory(skillsSrc) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let names = [];
|
|
20
|
+
try { names = readdirSync(skillsSrc); } catch { return out; }
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
try {
|
|
23
|
+
if (!statSync(join(skillsSrc, name)).isDirectory()) continue;
|
|
24
|
+
const md = readFileSync(join(skillsSrc, name, 'SKILL.md'), 'utf8');
|
|
25
|
+
const desc = (md.match(/^description:\s*(.+)$/m) || [])[1] || '';
|
|
26
|
+
out.push({ name, description: desc.trim() });
|
|
27
|
+
} catch { /* skill sem SKILL.md */ }
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderAgentsSection(skills) {
|
|
33
|
+
const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
|
|
34
|
+
return `${AG_START}
|
|
35
|
+
## wendkeep — process skills & loop
|
|
36
|
+
|
|
37
|
+
This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
|
|
38
|
+
through its change loop: \`wendkeep change new <slug>\` → implement tasks test-first
|
|
39
|
+
(tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
|
|
40
|
+
\`wendkeep verify --deep\` + an independent read-only verification pass writing
|
|
41
|
+
\`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
|
|
42
|
+
status\` / \`spec list\` / \`sensors list\`.
|
|
43
|
+
|
|
44
|
+
Process skills (full text in \`.claude/skills/\` and the vault's \`.brain/skills/\`):
|
|
45
|
+
${list}
|
|
46
|
+
${AG_END}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function upsertAgentsMd(projectPath, skillsSrc) {
|
|
50
|
+
const skills = skillInventory(skillsSrc);
|
|
51
|
+
if (!skills.length) return false;
|
|
52
|
+
const path = join(projectPath, 'AGENTS.md');
|
|
53
|
+
const section = renderAgentsSection(skills);
|
|
54
|
+
let content = '';
|
|
55
|
+
try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
|
|
56
|
+
if (content.includes(AG_START) && content.includes(AG_END)) {
|
|
57
|
+
const start = content.indexOf(AG_START);
|
|
58
|
+
const end = content.indexOf(AG_END) + AG_END.length;
|
|
59
|
+
content = content.slice(0, start) + section + content.slice(end);
|
|
60
|
+
} else {
|
|
61
|
+
content = content ? `${content.trimEnd()}\n\n${section}\n` : `${section}\n`;
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(path, content, 'utf8');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
11
67
|
export function syncDefs(vaultBase, projectPath) {
|
|
12
|
-
const out = { agents: [], skills: [] };
|
|
68
|
+
const out = { agents: [], skills: [], agentsMd: false };
|
|
13
69
|
|
|
14
70
|
const agentsSrc = join(vaultBase, '.brain', 'agents');
|
|
15
71
|
if (existsSync(agentsSrc)) {
|
|
@@ -34,6 +90,9 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
34
90
|
}
|
|
35
91
|
}
|
|
36
92
|
|
|
93
|
+
// Agent-agnostic channel: the managed AGENTS.md section (docs/17).
|
|
94
|
+
out.agentsMd = upsertAgentsMd(projectPath, skillsSrc);
|
|
95
|
+
|
|
37
96
|
return out;
|
|
38
97
|
}
|
|
39
98
|
|
package/src/taxonomy.mjs
CHANGED
package/src/validate-core.mjs
CHANGED
|
@@ -11,11 +11,20 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
11
11
|
const HARD_LIMIT = 25;
|
|
12
12
|
const SOFT_LIMIT = 22;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
]
|
|
14
|
+
// Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
|
|
15
|
+
// locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
|
|
16
|
+
const SECTION_SETS = {
|
|
17
|
+
'pt-BR': [
|
|
18
|
+
{ label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
|
|
19
|
+
{ label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
|
|
20
|
+
{ label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
|
|
21
|
+
],
|
|
22
|
+
en: [
|
|
23
|
+
{ label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
|
|
24
|
+
{ label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
|
|
25
|
+
{ label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
19
28
|
|
|
20
29
|
// Secret patterns reject only "real" values (length floor); abstract mentions like
|
|
21
30
|
// `sk_*` / `whsec_*` (trailing asterisk) are allowed.
|
|
@@ -41,9 +50,10 @@ export function validateCore(content) {
|
|
|
41
50
|
if (lineCount > HARD_LIMIT) {
|
|
42
51
|
errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
43
52
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Pick the locale set that matches best; require it to be complete.
|
|
54
|
+
const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
|
|
55
|
+
const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
|
|
56
|
+
for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
|
|
47
57
|
for (const { name, regex } of SECRET_PATTERNS) {
|
|
48
58
|
const m = text.match(regex);
|
|
49
59
|
if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
|
|
@@ -61,7 +71,22 @@ export function validateCore(content) {
|
|
|
61
71
|
|
|
62
72
|
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
63
73
|
// curated hot layer exists with the right shape from day one.
|
|
64
|
-
export function renderCoreSkeleton() {
|
|
74
|
+
export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
75
|
+
if (localeId === 'en') {
|
|
76
|
+
return `# CORE — curated memory core (.brain)
|
|
77
|
+
|
|
78
|
+
> RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
|
|
79
|
+
|
|
80
|
+
## User Preferences
|
|
81
|
+
- (durable preferences: language, style, conventions)
|
|
82
|
+
|
|
83
|
+
## Active Patterns
|
|
84
|
+
- (active patterns/architecture another agent must know)
|
|
85
|
+
|
|
86
|
+
## Open Items
|
|
87
|
+
- (open items/decisions — remove when resolved)
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
65
90
|
return `# CORE — núcleo curado da memória (.brain)
|
|
66
91
|
|
|
67
92
|
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
package/src/vault-theme.mjs
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
export const SNIPPET_NAME = 'wendkeep-colors';
|
|
10
10
|
|
|
11
|
+
import { LOCALES } from '../hooks/locale.mjs';
|
|
12
|
+
const PT = LOCALES['pt-BR'];
|
|
13
|
+
|
|
11
14
|
// Base palette (hex + "r, g, b" for rgba()).
|
|
12
15
|
const PALETTE = {
|
|
13
16
|
blue: { hex: '#2f80ed', rgb: '47, 128, 237' },
|
|
@@ -20,42 +23,49 @@ const PALETTE = {
|
|
|
20
23
|
slate: { hex: '#64748b', rgb: '100, 116, 139' },
|
|
21
24
|
};
|
|
22
25
|
|
|
23
|
-
// File-explorer folder -> palette key
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
// File-explorer folder KEY -> palette key; folder names resolve via the vault locale.
|
|
27
|
+
const PALETTE_BY_KEY = {
|
|
28
|
+
inbox: 'amber',
|
|
29
|
+
project: 'indigo',
|
|
30
|
+
sessions: 'blue',
|
|
31
|
+
linear: 'teal',
|
|
32
|
+
decisions: 'violet',
|
|
33
|
+
bugs: 'red',
|
|
34
|
+
learnings: 'green',
|
|
35
|
+
specs: 'teal',
|
|
36
|
+
changes: 'amber',
|
|
37
|
+
};
|
|
38
|
+
function folderPalette(loc) {
|
|
39
|
+
return [...Object.entries(PALETTE_BY_KEY).map(([k, p]) => [loc.folders[k], p]), ['Templates', 'slate']];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Note cssclass -> palette + locale folder key; `folder` resolves per locale.
|
|
43
|
+
const NOTE_TYPES = {
|
|
44
|
+
session: { cssClass: 'topic-session', folderKey: 'sessions', palette: 'blue' },
|
|
45
|
+
decision: { cssClass: 'topic-decision', folderKey: 'decisions', palette: 'violet' },
|
|
46
|
+
bug: { cssClass: 'topic-bug', folderKey: 'bugs', palette: 'red' },
|
|
47
|
+
learning: { cssClass: 'topic-learning', folderKey: 'learnings', palette: 'green' },
|
|
48
|
+
spec: { cssClass: 'topic-spec', folderKey: 'specs', palette: 'teal' },
|
|
49
|
+
change: { cssClass: 'topic-change', folderKey: 'changes', palette: 'amber' },
|
|
45
50
|
};
|
|
51
|
+
// Legacy pt-shaped export (folder names resolved at pt-BR) kept for compat.
|
|
52
|
+
export const NOTE_COLORS = Object.fromEntries(
|
|
53
|
+
Object.entries(NOTE_TYPES).map(([k, v]) => [k, { cssClass: v.cssClass, folder: PT.folders[v.folderKey], palette: v.palette }]),
|
|
54
|
+
);
|
|
46
55
|
|
|
47
|
-
// cssclasses that get the note-accent treatment (the
|
|
48
|
-
const TOPIC_CLASSES = [...Object.values(
|
|
56
|
+
// cssclasses that get the note-accent treatment (the note types + a home note).
|
|
57
|
+
const TOPIC_CLASSES = [...Object.values(NOTE_TYPES).map((n) => n.cssClass), 'vault-home'];
|
|
49
58
|
const NOTE_ACCENTS = [
|
|
50
|
-
...Object.values(
|
|
59
|
+
...Object.values(NOTE_TYPES).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
|
|
51
60
|
{ cssClass: 'vault-home', palette: 'indigo' },
|
|
52
61
|
];
|
|
53
62
|
|
|
54
63
|
const FX = '.workspace-leaf-content[data-type="file-explorer"] .nav-folder-title';
|
|
55
|
-
const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
|
|
56
64
|
const topicReading = TOPIC_CLASSES.map((c) => `.${c}`).join(', ');
|
|
57
65
|
|
|
58
|
-
export function renderColorSnippetCss() {
|
|
66
|
+
export function renderColorSnippetCss(loc = PT) {
|
|
67
|
+
const FOLDER_PALETTE = folderPalette(loc);
|
|
68
|
+
const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
|
|
59
69
|
const rootVars = Object.entries(PALETTE)
|
|
60
70
|
.map(([k, v]) => ` --us-${k}: ${v.hex};\n --us-${k}-rgb: ${v.rgb};`)
|
|
61
71
|
.join('\n');
|
|
@@ -181,9 +191,9 @@ export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
// Graph color groups: one per note folder, colored from the palette.
|
|
184
|
-
export function graphColorGroups() {
|
|
185
|
-
return Object.values(
|
|
186
|
-
query: `path:"${v.
|
|
194
|
+
export function graphColorGroups(loc = PT) {
|
|
195
|
+
return Object.values(NOTE_TYPES).map((v) => ({
|
|
196
|
+
query: `path:"${loc.folders[v.folderKey]}"`,
|
|
187
197
|
color: { a: 1, rgb: parseInt(PALETTE[v.palette].hex.slice(1), 16) },
|
|
188
198
|
}));
|
|
189
199
|
}
|
package/src/verify.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.m
|
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
import { tasksHashOf } from '../hooks/spec-core.mjs';
|
|
9
9
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
10
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
10
11
|
|
|
11
12
|
function today() {
|
|
12
13
|
const d = new Date();
|
|
@@ -28,7 +29,7 @@ export function runVerify(argv) {
|
|
|
28
29
|
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
29
30
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
30
31
|
|
|
31
|
-
const changeDir = join(vaultBase,
|
|
32
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
32
33
|
let tarefas = '';
|
|
33
34
|
try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
|
|
34
35
|
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|