wendkeep 0.6.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.6.1",
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": {
@@ -11,6 +11,7 @@
11
11
  "bin",
12
12
  "src",
13
13
  "hooks",
14
+ "schema",
14
15
  "README.md",
15
16
  "CHANGELOG.md"
16
17
  ],
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json",
4
+ "title": "wendkeep.sensors.json",
5
+ "description": "Native sensor config for the wendkeep harness (harness contract v1.1).",
6
+ "type": "object",
7
+ "required": ["version", "sensors"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "version": { "const": 1 },
11
+ "source": { "type": "string" },
12
+ "sensors": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "object",
16
+ "required": ["id", "command"],
17
+ "properties": {
18
+ "id": { "type": "string", "minLength": 1 },
19
+ "name": { "type": "string" },
20
+ "description": { "type": "string" },
21
+ "severity": { "enum": ["critical", "warning"], "default": "critical" },
22
+ "type": { "enum": ["command", "mutation"], "default": "command" },
23
+ "command": { "type": "string", "minLength": 1 },
24
+ "report": {
25
+ "type": "string",
26
+ "description": "type: mutation only — path (project-relative) to the mutation-testing-elements report."
27
+ }
28
+ },
29
+ "additionalProperties": false
30
+ }
31
+ }
32
+ },
33
+ "additionalProperties": false
34
+ }
package/src/change.mjs CHANGED
@@ -6,11 +6,13 @@ import {
6
6
  activeChange,
7
7
  listChanges,
8
8
  parseTasks,
9
+ setTaskDone,
9
10
  archiveChange,
10
11
  } from '../hooks/change-core.mjs';
11
12
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
12
- import { evaluateVerdict, tasksHashOf } from '../hooks/spec-core.mjs';
13
+ import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
13
14
  import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
15
+ import { getLocale } from '../hooks/locale.mjs';
14
16
 
15
17
  function resolveVault(argv) {
16
18
  let vault;
@@ -27,6 +29,13 @@ function resolveVault(argv) {
27
29
  return isAbsolute(base) ? base : resolve(process.cwd(), base);
28
30
  }
29
31
 
32
+ function opt(argv, name) {
33
+ const i = argv.indexOf(name);
34
+ if (i >= 0) return argv[i + 1];
35
+ const eq = argv.find((a) => a.startsWith(`${name}=`));
36
+ return eq ? eq.slice(name.length + 1) : undefined;
37
+ }
38
+
30
39
  function today() {
31
40
  const d = new Date();
32
41
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
@@ -35,7 +44,8 @@ function today() {
35
44
  export function runChange(argv) {
36
45
  const [sub, ...rest] = argv;
37
46
  const vaultBase = resolveVault(rest);
38
- const slugArg = () => rest.find((a) => !a.startsWith('-'));
47
+ const VALUE_FLAGS = new Set(['--vault', '--change', '--project']);
48
+ const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
39
49
 
40
50
  if (sub === 'new') {
41
51
  const slug = slugArg();
@@ -60,7 +70,7 @@ export function runChange(argv) {
60
70
  const slug = slugArg();
61
71
  if (!slug) { process.stderr.write('wendkeep change show: missing <slug>\n'); process.exit(2); }
62
72
  let md;
63
- try { md = readFileSync(join(vaultBase, '08-Mudanças', slug, 'tarefas.md'), 'utf8'); }
73
+ try { md = readFileSync(join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'tarefas.md'), 'utf8'); }
64
74
  catch { process.stderr.write(`wendkeep change show: not found: ${slug}\n`); process.exit(2); }
65
75
  const tasks = parseTasks(md);
66
76
  const open = tasks.filter((t) => !t.done).length;
@@ -69,6 +79,76 @@ export function runChange(argv) {
69
79
  process.exit(0);
70
80
  }
71
81
 
82
+ if (sub === 'status') {
83
+ const slug = slugArg() || activeChange(vaultBase);
84
+ if (!slug) { process.stderr.write('wendkeep change status: no change (arg or active)\n'); process.exit(2); }
85
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
86
+ let tarefasMd;
87
+ try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); }
88
+ catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
89
+ const tasks = parseTasks(tarefasMd);
90
+ const done = tasks.filter((t) => t.done).length;
91
+ process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase) ? ' (ativa)' : ''}\n`);
92
+ let specs = [];
93
+ try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); } catch { /* sem proposta */ }
94
+ process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
95
+ process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
96
+ for (const t of tasks) {
97
+ process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.req ? ` [req:${t.req}]` : ''}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
98
+ }
99
+ let evidence = null;
100
+ try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
101
+ if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
102
+ else process.stdout.write('evidencia: ausente\n');
103
+ const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
104
+ let verdict = null;
105
+ try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
106
+ if (!reqIds.length) process.stdout.write('verdict: não exigido (sem [req:])\n');
107
+ else if (!verdict) process.stdout.write('verdict: ausente — rode `wendkeep verify --deep` + wk-verify\n');
108
+ else {
109
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
110
+ process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
111
+ }
112
+ try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
113
+ process.exit(0);
114
+ }
115
+
116
+ if (sub === 'done' || sub === 'undone') {
117
+ const taskId = slugArg();
118
+ if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
119
+ const slug = opt(rest, '--change') || activeChange(vaultBase);
120
+ if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
121
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
122
+ let ok = false;
123
+ try { ok = setTaskDone(dir, taskId, sub === 'done'); } catch { /* sem tarefas.md */ }
124
+ if (!ok) { process.stderr.write(`wendkeep change ${sub}: task não encontrada: ${taskId}\n`); process.exit(2); }
125
+ process.stdout.write(`task ${taskId}: ${sub === 'done' ? '[x]' : '[ ]'}\n`);
126
+ process.exit(0);
127
+ }
128
+
129
+ if (sub === 'diff') {
130
+ const slug = slugArg() || activeChange(vaultBase);
131
+ if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
132
+ const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
133
+ let specs = [];
134
+ try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); }
135
+ catch { process.stderr.write(`wendkeep change diff: not found: ${slug}\n`); process.exit(2); }
136
+ if (!specs.length) { process.stdout.write('diff: sem specs declaradas na proposta\n'); process.exit(0); }
137
+ for (const cap of specs) {
138
+ let delta;
139
+ try { delta = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8')); }
140
+ catch { process.stdout.write(`! sem delta para ${cap}\n`); continue; }
141
+ process.stdout.write(`spec: ${cap}\n`);
142
+ for (const r of delta.added) process.stdout.write(` + ${r.id || r.name} (ADDED)\n`);
143
+ for (const r of delta.modified) process.stdout.write(` ~ ${r.id || r.name} (MODIFIED)\n`);
144
+ for (const k of delta.removed) process.stdout.write(` - ${k} (REMOVED)\n`);
145
+ let living = [];
146
+ try { living = parseRequirements(readFileSync(join(vaultBase, getLocale(vaultBase).folders.specs, `${cap}.md`), 'utf8')); } catch { /* nova capability */ }
147
+ for (const w of applyDelta(living, delta).warnings) process.stdout.write(` ! ${w}\n`);
148
+ }
149
+ process.exit(0);
150
+ }
151
+
72
152
  if (sub === 'archive') {
73
153
  const slug = slugArg() || activeChange(vaultBase);
74
154
  if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
@@ -109,6 +189,6 @@ export function runChange(argv) {
109
189
  process.exit(0);
110
190
  }
111
191
 
112
- process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, archive.\n`);
192
+ process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, status, done, undone, diff, archive.\n`);
113
193
  process.exit(2);
114
194
  }
@@ -37,7 +37,7 @@ export function renderSensorsJson(scripts = {}) {
37
37
  });
38
38
  }
39
39
  }
40
- return `${JSON.stringify({ version: 1, source: 'manual', sensors }, null, 2)}\n`;
40
+ return `${JSON.stringify({ $schema: 'https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json', version: 1, source: 'manual', sensors }, null, 2)}\n`;
41
41
  }
42
42
 
43
43
  function readProjectScripts(projectPath) {
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 });
@@ -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, '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 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: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain + change/spec + sensors seeded`);
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);
@@ -0,0 +1,23 @@
1
+ // `wendkeep sensors list` — read-only view over wendkeep.sensors.json (0.7.0).
2
+ import { resolve } from 'node:path';
3
+ import { loadSensors } from '../hooks/sensors-core.mjs';
4
+
5
+ export function runSensors(argv) {
6
+ const [sub, ...rest] = argv;
7
+ if (sub !== 'list') {
8
+ process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list.\n`);
9
+ process.exit(2);
10
+ }
11
+ let project;
12
+ for (let i = 0; i < rest.length; i += 1) {
13
+ if (rest[i] === '--project') project = rest[++i];
14
+ else if (rest[i].startsWith('--project=')) project = rest[i].slice(10);
15
+ }
16
+ const projectRoot = resolve(project || process.cwd());
17
+ const sensors = loadSensors(projectRoot);
18
+ if (!sensors.length) { process.stdout.write('sensors: (nenhum — crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
19
+ for (const s of sensors) {
20
+ process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
21
+ }
22
+ process.exit(0);
23
+ }
package/src/spec.mjs ADDED
@@ -0,0 +1,54 @@
1
+ // `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { parseRequirements } from '../hooks/spec-core.mjs';
5
+ import { getLocale } from '../hooks/locale.mjs';
6
+
7
+ function resolveVault(argv) {
8
+ let vault;
9
+ for (let i = 0; i < argv.length; i += 1) {
10
+ const a = argv[i];
11
+ if (a === '--vault') vault = argv[++i];
12
+ else if (a.startsWith('--vault=')) vault = a.slice(8);
13
+ }
14
+ const base = vault || process.env.OBSIDIAN_VAULT_PATH;
15
+ if (!base) {
16
+ process.stderr.write('wendkeep spec: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
17
+ process.exit(2);
18
+ }
19
+ return isAbsolute(base) ? base : resolve(process.cwd(), base);
20
+ }
21
+
22
+ export function runSpec(argv) {
23
+ const [sub, ...rest] = argv;
24
+ const vaultBase = resolveVault(rest);
25
+ const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
26
+
27
+ if (sub === 'list') {
28
+ let files = [];
29
+ try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
30
+ if (!files.length) { process.stdout.write('specs: (nenhuma)\n'); process.exit(0); }
31
+ for (const f of files) {
32
+ const md = readFileSync(join(specsDir, f), 'utf8');
33
+ const n = parseRequirements(md).length;
34
+ const upd = md.match(/> Atualizado por .* em (\d{4}-\d{2}-\d{2})/);
35
+ process.stdout.write(`${f.replace(/\.md$/, '')}: ${n} requisito(s)${upd ? ` (atualizado ${upd[1]})` : ''}\n`);
36
+ }
37
+ process.exit(0);
38
+ }
39
+
40
+ if (sub === 'show') {
41
+ const cap = rest.find((a, i) => !a.startsWith('-') && rest[i - 1] !== '--vault');
42
+ if (!cap) { process.stderr.write('wendkeep spec show: missing <capability>\n'); process.exit(2); }
43
+ let md;
44
+ try { md = readFileSync(join(specsDir, `${cap}.md`), 'utf8'); }
45
+ catch { process.stderr.write(`wendkeep spec show: not found: ${cap}\n`); process.exit(2); }
46
+ const reqs = parseRequirements(md);
47
+ process.stdout.write(`${cap}: ${reqs.length} requisito(s)\n`);
48
+ for (const r of reqs) process.stdout.write(` ${r.id ? `${r.id} — ` : ''}${r.name}\n`);
49
+ process.exit(0);
50
+ }
51
+
52
+ process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
53
+ process.exit(2);
54
+ }
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>.
@@ -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 (mirrors VAULT_FOLDERS in taxonomy.mjs).
24
- const FOLDER_PALETTE = [
25
- ['00-Inbox', 'amber'],
26
- ['01-Projeto', 'indigo'],
27
- ['02-Sessões', 'blue'],
28
- ['03-Linear', 'teal'],
29
- ['04-Decisões', 'violet'],
30
- ['05-Bugs', 'red'],
31
- ['06-Aprendizados', 'green'],
32
- ['07-Specs', 'teal'],
33
- ['08-Mudanças', 'amber'],
34
- ['Templates', 'slate'],
35
- ];
36
-
37
- // Note cssclass -> palette key; `folder` is reused for the graph color groups.
38
- export const NOTE_COLORS = {
39
- session: { cssClass: 'topic-session', folder: '02-Sessões', palette: 'blue' },
40
- decision: { cssClass: 'topic-decision', folder: '04-Decisões', palette: 'violet' },
41
- bug: { cssClass: 'topic-bug', folder: '05-Bugs', palette: 'red' },
42
- learning: { cssClass: 'topic-learning', folder: '06-Aprendizados', palette: 'green' },
43
- spec: { cssClass: 'topic-spec', folder: '07-Specs', palette: 'teal' },
44
- change: { cssClass: 'topic-change', folder: '08-Mudanças', palette: 'amber' },
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 four note types + a home note).
48
- const TOPIC_CLASSES = [...Object.values(NOTE_COLORS).map((n) => n.cssClass), 'vault-home'];
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(NOTE_COLORS).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
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(NOTE_COLORS).map((v) => ({
186
- query: `path:"${v.folder}"`,
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, '08-Mudanças', slug);
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); }