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/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 VAULT_FOLDERS) {
274
+ for (const f of folders) {
259
275
  const p = join(vaultPath, f);
260
276
  if (!existsSync(p)) {
261
277
  mkdirSync(p, { recursive: true });
@@ -266,7 +282,7 @@ export async function runInit(argv) {
266
282
  const readmePath = join(vaultPath, 'README.md');
267
283
  let readmeNote = '';
268
284
  if (!existsSync(readmePath)) {
269
- writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
285
+ writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp, locale: loc.id }), 'utf8');
270
286
  readmeNote = ', README.md created';
271
287
  }
272
288
  // Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
@@ -275,18 +291,27 @@ 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
282
298
  // truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
283
299
  seedDefinitions(brainDir);
284
- seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
300
+ seedWkSkills(brainDir, loc.id); // Pilar A: native process skills, in the vault locale.
285
301
  // Seed the change/spec layer starters (Pilar B) — non-destructive.
286
- const specsReadme = join(vaultPath, '07-Specs', 'README.md');
287
- if (!existsSync(specsReadme)) writeFileSync(specsReadme, '# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em `08-Mudanças/` promovem deltas aqui no `wendkeep change archive`.\n', 'utf8');
302
+ const en = loc.id === 'en';
303
+ const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
304
+ if (!existsSync(specsReadme)) {
305
+ writeFileSync(specsReadme, en
306
+ ? `# Specs — living contract\n\nThe project's capabilities (requirements/scenarios). Changes in \`${loc.folders.changes}/\` promote deltas here on \`wendkeep change archive\`.\n`
307
+ : `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
308
+ }
288
309
  const changeTpl = join(vaultPath, 'Templates', 'Change.md');
289
- if (!existsSync(changeTpl)) writeFileSync(changeTpl, '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
310
+ if (!existsSync(changeTpl)) {
311
+ writeFileSync(changeTpl, en
312
+ ? '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Why\n\n## What changes\n'
313
+ : '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
314
+ }
290
315
  // Seed the native sensor config (Pilar C) at project root — non-destructive.
291
316
  const sensorsFile = join(projectPath, 'wendkeep.sensors.json');
292
317
  if (!existsSync(sensorsFile)) {
@@ -294,7 +319,7 @@ export async function runInit(argv) {
294
319
  try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
295
320
  writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
296
321
  }
297
- log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain + change/spec + sensors seeded`);
322
+ log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded`);
298
323
  // Deliver the seeded defs (agents + wk process skills) to the project so they're
299
324
  // usable immediately — no separate `wendkeep sync-defs` step needed.
300
325
  const synced = syncDefs(vaultPath, projectPath);
@@ -203,7 +203,135 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
203
203
  - O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
204
204
  `;
205
205
 
206
- export const WK_SKILLS = [
206
+ const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
207
+
208
+ Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
209
+ (sensors) together, wikilinked in the Obsidian graph.
210
+
211
+ ## Steps
212
+
213
+ 1. **Explore** — understand the problem before proposing.
214
+ 2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
215
+ (proposta/design/tarefas + a \`specs/\` delta). The change becomes *active* and is
216
+ injected at the next SessionStart.
217
+ 3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
218
+ \`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
219
+ 4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
220
+ the verification package.
221
+ 5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
222
+ A trivial change (no \`[req:]\`) gets an auto verdict.
223
+ 6. **Archive** — \`wendkeep change archive <slug>\`. The gate needs green sensors AND a
224
+ verdict AND no open tasks. It promotes the delta into \`07-Specs\` and mints an ADR.
225
+
226
+ ## Rules
227
+ - One active change at a time. Finish (archive) before starting another.
228
+ - No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
229
+ - The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
230
+ `;
231
+
232
+ const TDD_EN = `# TDD — Red, Green, Refactor (tests that discriminate)
233
+
234
+ Never write production code without a red test asking for it — and never write a test that
235
+ would pass under the wrong implementation.
236
+
237
+ ## The cycle
238
+ 1. **Red** — the smallest test that fails for the missing behaviour.
239
+ 2. **See it fail** for the right reason (not an import/typo).
240
+ 3. **Green** — the minimal code to pass.
241
+ 4. **Refactor** with the greens protecting you.
242
+
243
+ ## Derive from the spec, not the code
244
+ Write assertions from the requirement's acceptance criteria (\`07-Specs\`), not by reading the
245
+ implementation. Reading the code to write the test = it only confirms what the code already does.
246
+
247
+ ## Non-shallow litmus
248
+ Reject an assertion that would pass under a wrong implementation. Assert a **value or persisted
249
+ state**, never "the mock was called". "Tested later" is rejected.
250
+
251
+ ## Adequacy (necessary and sufficient)
252
+ Every acceptance criterion has an assertion with \`file:line\` evidence; every test traces to a
253
+ requirement (\`[req:ID]\`); covers what the spec asks, nothing more.
254
+
255
+ ## Learn the project
256
+ Sample 5–10 existing tests for style/location/framework/depth (treat that depth as a floor).
257
+ Read \`AGENTS.md\` / \`.cursor/rules\` / CI configs for declared standards.
258
+ `;
259
+
260
+ const DEBUGGING_EN = `# Systematic debugging
261
+
262
+ Use it when something fails or behaves wrong. One hypothesis at a time — no shotgun changes.
263
+
264
+ 1. **Reproduce** — a minimal deterministic case. Without it you can't know you fixed anything.
265
+ 2. **One hypothesis** — the most likely cause, specific enough to test.
266
+ 3. **Isolate** — confirm or kill the hypothesis BEFORE fixing (bisect, log, comment half). Prove
267
+ where it is, not where you think it is.
268
+ 4. **Fix the root**, not the symptom.
269
+ 5. **Verify** — the repro is gone AND the suite stays green. No regression.
270
+
271
+ Changed several things and it "worked"? You don't know what fixed it — revert, isolate one
272
+ variable. Read the whole error + stack — the decisive line is usually there.
273
+ `;
274
+
275
+ const BRAINSTORMING_EN = `# Brainstorming — idea into design
276
+
277
+ Use it when the idea is still vague. Turn it into an approved design BEFORE writing code.
278
+
279
+ 1. **Explore context** — files, docs, relevant history.
280
+ 2. **One question at a time** — purpose, constraints, success criteria. Prefer multiple choice.
281
+ 3. **Propose 2–3 approaches** with trade-offs and your recommendation.
282
+ 4. **Present the design in sections**, confirm each.
283
+
284
+ ## Closure gate — no dangling ambiguity
285
+ Resolve every gray area: decide with the user, or log a **signed-off assumption** ("assuming X
286
+ because Y — correct me if wrong"). A declined gray area is recorded, not silently dropped.
287
+
288
+ ## Out-of-scope table
289
+ List explicitly what the change does **not** do. Undeclared scope becomes creep.
290
+
291
+ ## Hard gate
292
+ No code / scaffold / implementation action until a design is presented and approved. Then go to
293
+ **wk-planning**.
294
+ `;
295
+
296
+ const PLANNING_EN = `# Planning — design into a task plan
297
+
298
+ Use it after an approved design. Produce a plan an engineer with no project context can execute.
299
+
300
+ ## Before tasks
301
+ Map the files: what to create/modify and each one's responsibility. Files that change together
302
+ live together; one file, one responsibility.
303
+
304
+ ## Bite-sized tasks (TDD)
305
+ Each task ends in an independently testable deliverable. Each step is a 2–5 min action:
306
+ write the failing test (show code) → run and see it fail (exact command + expected) → minimal
307
+ implementation (show code) → run and see it pass → checkpoint: suite green.
308
+
309
+ ## Rules
310
+ - Exact file paths, always. Real code in each step — no "TODO", "handle errors appropriately",
311
+ "similar to Task N". DRY, YAGNI. Consistent names/signatures across tasks.
312
+ `;
313
+
314
+ const VERIFY_EN = `# Independent verification — the fresh pass
315
+
316
+ Use it in the *verify deep* step, after \`wendkeep verify --deep\`. You are the **verifier**, not
317
+ the author — even if you wrote the code, enter as if you'd never seen it. Fresh context, read-only.
318
+
319
+ ## What to do
320
+ 1. Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence).
321
+ 2. **Re-derive coverage from \`07-Specs\`** — for each \`[req:ID]\`, check the requirement's behaviour
322
+ is covered by a test that discriminates (wouldn't pass under a wrong impl). \`file:line\` evidence.
323
+ 3. Spec-anchored outcome check: does the observable result match the acceptance criterion?
324
+ 4. Write \`08-Changes/<slug>/verdict.json\`:
325
+ \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "notes": [] }\`.
326
+
327
+ ## Rules
328
+ - **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
329
+ - \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
330
+ - Don't fix here — a gap becomes a fix task; re-verify after.
331
+ - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
332
+ `;
333
+
334
+ const WK_SKILLS_PT = [
207
335
  skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
208
336
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
209
337
  skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
@@ -212,10 +340,25 @@ export const WK_SKILLS = [
212
340
  skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
213
341
  ];
214
342
 
343
+ const WK_SKILLS_EN = [
344
+ skill('wk-workflow', 'Use when starting any non-trivial change — orchestrates the a2 loop (explore, propose, apply, verify, archive) over the wendkeep commands.', WORKFLOW_EN),
345
+ skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
346
+ skill('wk-debugging', 'Use when something fails or breaks — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
347
+ skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN),
348
+ skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN),
349
+ skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN),
350
+ ];
351
+
352
+ // Skill set for a locale. WK_SKILLS stays the pt-BR set for back-compat.
353
+ export function wkSkills(localeId = 'pt-BR') {
354
+ return localeId === 'en' ? WK_SKILLS_EN : WK_SKILLS_PT;
355
+ }
356
+ export const WK_SKILLS = WK_SKILLS_PT;
357
+
215
358
  // Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
216
- export function seedWkSkills(brainDir) {
359
+ export function seedWkSkills(brainDir, localeId = 'pt-BR') {
217
360
  const created = [];
218
- for (const s of WK_SKILLS) {
361
+ for (const s of wkSkills(localeId)) {
219
362
  const dir = join(brainDir, 'skills', s.name);
220
363
  mkdirSync(dir, { recursive: true });
221
364
  const f = join(dir, 'SKILL.md');
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, '07-Specs');
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
@@ -25,6 +25,7 @@ export const VAULT_FOLDERS = [
25
25
  // entrypoints + price table). Mirrors the list the old setup-vault.ps1 copied.
26
26
  export const HOOK_FILES = [
27
27
  'obsidian-common.mjs',
28
+ 'locale.mjs',
28
29
  'session-start.mjs',
29
30
  'session-ensure.mjs',
30
31
  'session-stop.mjs',
@@ -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
- const REQUIRED_SECTIONS = [
15
- { label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
16
- { label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
17
- { label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
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
- for (const { label, regex } of REQUIRED_SECTIONS) {
45
- if (!regex.test(text)) errors.push(`Seção obrigatória ausente: ## ${label}`);
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>.
@@ -1,46 +1,90 @@
1
1
  // Renders a neutral, project-templated README for a freshly created wendkeep vault.
2
- // The structure table is derived from VAULT_FOLDERS so it can never drift from the
3
- // folders `wendkeep init` actually creates.
4
- import { VAULT_FOLDERS } from './taxonomy.mjs';
5
-
6
- const FOLDER_DESCRIPTIONS = {
7
- '00-Inbox': 'Notas rápidas, captura sem classificação',
8
- '01-Projeto': 'Arquitetura, padrões, memória do projeto',
9
- '02-Sessões': 'Uma nota por sessão de trabalho com o agente IA',
10
- '03-Linear': 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
11
- '04-Decisões': 'Architecture Decision Records (ADRs)',
12
- '05-Bugs': 'Bugs resolvidos com causa raiz documentada',
13
- '06-Aprendizados': 'Lições extraídas das sessões',
14
- Templates: 'Templates para novas notas',
15
- '.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
2
+ // Locale-aware (0.8.1): the structure table + prose follow the vault locale; folder names
3
+ // come from the locale so they can never drift from what `wendkeep init` creates.
4
+ import { LOCALES, vaultFolders } from '../hooks/locale.mjs';
5
+
6
+ // Folder descriptions keyed by locale folder KEY (not literal name), per locale.
7
+ const DESC = {
8
+ 'pt-BR': {
9
+ inbox: 'Notas rápidas, captura sem classificação',
10
+ project: 'Arquitetura, padrões, memória do projeto',
11
+ sessions: 'Uma nota por sessão de trabalho com o agente IA',
12
+ linear: 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
13
+ decisions: 'Architecture Decision Records (ADRs)',
14
+ bugs: 'Bugs resolvidos com causa raiz documentada',
15
+ learnings: 'Lições extraídas das sessões',
16
+ specs: 'Contrato vivo: capacidades do projeto (requisitos)',
17
+ changes: 'Mudanças em andamento (proposta/design/tarefas/spec-delta)',
18
+ Templates: 'Templates para novas notas',
19
+ '.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
20
+ },
21
+ en: {
22
+ inbox: 'Quick notes, unclassified capture',
23
+ project: 'Architecture, patterns, project memory',
24
+ sessions: 'One note per AI-agent work session',
25
+ linear: 'Mirror of issues/tasks from your tracker (Linear, Jira, GitHub Issues…)',
26
+ decisions: 'Architecture Decision Records (ADRs)',
27
+ bugs: 'Fixed bugs with documented root cause',
28
+ learnings: 'Lessons extracted from sessions',
29
+ specs: 'Living contract: the project capabilities (requirements)',
30
+ changes: 'Changes in flight (proposal/design/tasks/spec-delta)',
31
+ Templates: 'Templates for new notes',
32
+ '.brain': 'Canonical memory: CORE.md (curated) + DIGEST.md (auto), injected at SessionStart',
33
+ },
16
34
  };
17
35
 
18
- function structureTable() {
19
- const rows = VAULT_FOLDERS.map((f) => {
20
- const desc = FOLDER_DESCRIPTIONS[f] || 'Notas do projeto';
21
- return `| \`${f}/\` | ${desc} |`;
22
- });
23
- return ['| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
36
+ // folder literal -> key, for description lookup.
37
+ function keyForFolder(loc, folder) {
38
+ const entry = Object.entries(loc.folders).find(([, name]) => name === folder);
39
+ return entry ? entry[0] : folder; // Templates / .brain fall through as themselves
24
40
  }
25
41
 
26
- export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
27
- const name = projectName || 'projeto';
28
- const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
42
+ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, locale = 'pt-BR' }) {
43
+ const loc = LOCALES[locale] || LOCALES['pt-BR'];
44
+ const en = loc.id === 'en';
45
+ const desc = DESC[loc.id] || DESC['pt-BR'];
46
+ const name = projectName || (en ? 'project' : 'projeto');
47
+
48
+ const rows = vaultFolders(loc).map((f) => `| \`${f}/\` | ${desc[keyForFolder(loc, f)] || (en ? 'Project notes' : 'Notas do projeto')} |`);
49
+ const table = [en ? '| Folder | Contents |' : '| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
50
+
51
+ if (en) {
52
+ const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
53
+ const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
54
+ if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
55
+ access.push('- **Hooks:** `settings.json` calls `npx wendkeep hook <name>`; the vault is located via `OBSIDIAN_VAULT_PATH` (also written by `wendkeep init`).');
56
+ return `# Obsidian vault — ${name}
57
+
58
+ > Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
59
+ > sessions (**Claude Code**, **Codex**) via session hooks${mcpIntro}.
60
+
61
+ ## Structure
62
+
63
+ ${table}
64
+
65
+ ## Access
29
66
 
30
- const access = [
31
- `- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``,
32
- ];
33
- if (withMcp) {
34
- access.push(
35
- '- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault\n' +
36
- ' pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.',
37
- );
67
+ ${access.join('\n')}
68
+
69
+ ## Rules
70
+
71
+ 1. Every relevant work session produces a note in \`${loc.folders.sessions}/\` (automatic, via hooks).
72
+ 2. Architecture decisions become ADRs in \`${loc.folders.decisions}/\`.
73
+ 3. Bugs with a root cause go to \`${loc.folders.bugs}/\`; lessons to \`${loc.folders.learnings}/\`.
74
+ 4. Canonical memory: \`.brain/CORE.md\` (curated) + \`.brain/DIGEST.md\` (auto) — injected at SessionStart.
75
+ 5. Tags in YAML frontmatter, not inline. Wikilinks \`[[note]]\` to connect notes.
76
+
77
+ ## Notes
78
+
79
+ - **Locale:** this vault is \`en\` (folder names in English). Set at \`wendkeep init --locale\`; a vault is never renamed after.
80
+ - **Versioning:** session notes may contain transcript excerpts — review before committing this vault to a shared repo.
81
+ `;
38
82
  }
39
- access.push(
40
- '- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado\n' +
41
- ' via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).',
42
- );
43
83
 
84
+ const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
85
+ const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
86
+ if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
87
+ access.push('- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).');
44
88
  return `# Vault Obsidian — ${name}
45
89
 
46
90
  > Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
@@ -49,7 +93,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
49
93
 
50
94
  ## Estrutura
51
95
 
52
- ${structureTable()}
96
+ ${table}
53
97
 
54
98
  ## Acesso
55
99
 
@@ -57,17 +101,15 @@ ${access.join('\n')}
57
101
 
58
102
  ## Regras
59
103
 
60
- 1. Toda sessão de trabalho relevante gera nota em \`02-Sessões/\` (automático, pelos hooks).
61
- 2. Decisões de arquitetura viram ADR em \`04-Decisões/\`.
62
- 3. Bugs com causa raiz vão para \`05-Bugs/\`; lições para \`06-Aprendizados/\`.
104
+ 1. Toda sessão de trabalho relevante gera nota em \`${loc.folders.sessions}/\` (automático, pelos hooks).
105
+ 2. Decisões de arquitetura viram ADR em \`${loc.folders.decisions}/\`.
106
+ 3. Bugs com causa raiz vão para \`${loc.folders.bugs}/\`; lições para \`${loc.folders.learnings}/\`.
63
107
  4. Memória canônica: \`.brain/CORE.md\` (curado) + \`.brain/DIGEST.md\` (auto) — injetados no SessionStart.
64
- 5. Tags em frontmatter YAML, não inline.
65
- 6. Wikilinks \`[[nota]]\` para conectar notas entre si.
108
+ 5. Tags em frontmatter YAML, não inline. Wikilinks \`[[nota]]\` para conectar notas.
66
109
 
67
110
  ## Notas
68
111
 
69
- - **Idioma das pastas:** os nomes são PT-BR (limitação conhecida; i18n no roadmap do wendkeep).
70
- - **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie
71
- antes de commitar este vault em um repositório compartilhado.
112
+ - **Locale:** este vault é \`pt-BR\` (nomes de pasta em português). Definido em \`wendkeep init --locale\`; o vault nunca é renomeado depois.
113
+ - **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie antes de commitar este vault em um repositório compartilhado.
72
114
  `;
73
115
  }