wendkeep 0.2.5 → 0.2.6

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/README.md CHANGED
@@ -40,6 +40,8 @@ npx wendkeep init
40
40
  3. Add the `wendkeep-vault` MCP server to `.mcp.json` (skip with `--no-mcp`).
41
41
  4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` as an `.mcp.json` MCP server (any agent), `understand-anything` via a `understand-inject` SessionStart hook that injects its domain graph when generated, and the Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) as a bonus. Control with `--companions <csv>` or `--no-companions`. (`caveman` additionally runs its own cross-agent installer on non-Claude agents.)
42
42
  5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
43
+ 6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
44
+ 7. Seed the **definitions layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth for custom agents/skills, with a README + one example each). Run `wendkeep sync-defs` to copy them into the project's agent dirs (`.brain/agents/*.toml` → `.codex/agents/`, `.brain/skills/<name>/` → `.claude/skills/`).
43
45
 
44
46
  ```bash
45
47
  npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
package/bin/wendkeep.mjs CHANGED
@@ -40,6 +40,12 @@ Usage:
40
40
  agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
41
41
 
42
42
  wendkeep doctor [--vault P] Run a vault health check.
43
+ wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
44
+ protocol (cap 25, 3 sections, no secrets/PII). Uses
45
+ --vault <path> or OBSIDIAN_VAULT_PATH if no path given.
46
+ wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
47
+ project: .brain/agents/*.toml -> .codex/agents,
48
+ .brain/skills/<name> -> .claude/skills. --vault P --project P.
43
49
  wendkeep --version Print version.
44
50
  wendkeep --help Show this help.
45
51
  `;
@@ -80,6 +86,16 @@ async function main() {
80
86
  runDoctor(rest);
81
87
  break;
82
88
  }
89
+ case 'validate-memory': {
90
+ const { runValidateMemory } = await import('../src/validate-core.mjs');
91
+ runValidateMemory(rest);
92
+ break;
93
+ }
94
+ case 'sync-defs': {
95
+ const { runSyncDefs } = await import('../src/sync-defs.mjs');
96
+ runSyncDefs(rest);
97
+ break;
98
+ }
83
99
  case '--version':
84
100
  case '-v':
85
101
  process.stdout.write(`${version()}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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/init.mjs CHANGED
@@ -3,8 +3,7 @@
3
3
  // OBSIDIAN_VAULT_PATH into .claude/settings.json, and adds the mcpvault server to
4
4
  // .mcp.json. Idempotent: re-running only adds what is missing.
5
5
  import { spawnSync } from 'node:child_process';
6
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
- import { tmpdir } from 'node:os';
6
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
7
  import { basename, isAbsolute, join, resolve } from 'node:path';
9
8
  import { createInterface } from 'node:readline/promises';
10
9
  import {
@@ -19,8 +18,7 @@ import {
19
18
  companionSettingsPatch,
20
19
  companionMcpPatch,
21
20
  companionHookSpecs,
22
- cavemanInstallerCommand,
23
- cavemanInstallerUrl,
21
+ cavemanInstallCommand,
24
22
  } from './taxonomy.mjs';
25
23
  import { renderVaultReadme } from './vault-readme.mjs';
26
24
  import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
@@ -31,6 +29,8 @@ import {
31
29
  graphColorGroups,
32
30
  mergeGraphColorGroups,
33
31
  } from './vault-theme.mjs';
32
+ import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
33
+ import { seedDefinitions } from './sync-defs.mjs';
34
34
 
35
35
  function parseArgs(argv) {
36
36
  const args = { mcp: true, yes: false, force: false };
@@ -132,25 +132,16 @@ export function mergeMcp(existing, { vaultPath, withVault = true, companions = [
132
132
  // script to a temp file and runs it as a FILE — piping to iex/bash leaves the
133
133
  // script's self-path null and the installer aborts. Best-effort, fail-soft: the
134
134
  // Claude Code plugin entry is already wired regardless.
135
- async function runCavemanInstaller(log) {
136
- const url = cavemanInstallerUrl();
137
- const ext = process.platform === 'win32' ? 'ps1' : 'sh';
138
- const tmp = join(tmpdir(), `wendkeep-caveman-install-${process.pid}.${ext}`);
139
- log(`\n caveman: fetching cross-agent installer (best-effort): ${url}`);
135
+ function runCavemanInstaller(log) {
136
+ const cmd = cavemanInstallCommand();
137
+ log(`\n caveman: running cross-agent installer (best-effort, Gemini skipped):\n ${cmd}`);
140
138
  try {
141
- const res = await fetch(url);
142
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
143
- writeFileSync(tmp, await res.text(), 'utf8');
144
- const cmd = cavemanInstallerCommand(process.platform, tmp);
145
- log(` caveman: running ${cmd.command} ${cmd.args.join(' ')}`);
146
- const r = spawnSync(cmd.command, cmd.args, { stdio: 'inherit' });
139
+ const r = spawnSync(cmd, { stdio: 'inherit', shell: true });
147
140
  if (r.status !== 0) {
148
141
  log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
149
142
  }
150
143
  } catch (e) {
151
144
  log(` [!] caveman installer skipped (${e.message}) — Claude Code plugin entry still wired.`);
152
- } finally {
153
- try { rmSync(tmp, { force: true }); } catch { /* temp cleanup best-effort */ }
154
145
  }
155
146
  }
156
147
 
@@ -255,7 +246,19 @@ export async function runInit(argv) {
255
246
  writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
256
247
  readmeNote = ', README.md created';
257
248
  }
258
- log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
249
+ // Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
250
+ // DIGEST/index are auto-generated by the hooks; CORE is hand-curated, so we
251
+ // bootstrap it with the 3 required sections. Non-destructive.
252
+ const brainDir = join(vaultPath, '.brain');
253
+ mkdirSync(brainDir, { recursive: true });
254
+ const corePath = join(brainDir, 'CORE.md');
255
+ if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
256
+ const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
257
+ if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
258
+ // Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
259
+ // truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
260
+ seedDefinitions(brainDir);
261
+ log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain seeded (CORE + agents/skills)`);
259
262
 
260
263
  // 2. .claude/settings.json --------------------------------------------------
261
264
  const settingsPath = join(projectPath, '.claude', 'settings.json');
@@ -304,7 +307,7 @@ export async function runInit(argv) {
304
307
  // caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
305
308
  // from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
306
309
  // plugin entry is already wired in settings.json regardless.
307
- if (companions.includes('caveman')) await runCavemanInstaller(log);
310
+ if (companions.includes('caveman')) runCavemanInstaller(log);
308
311
 
309
312
  log('\nNext steps:');
310
313
  log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
@@ -0,0 +1,123 @@
1
+ // Definitions layer: versioned custom agents/skills live in the vault's .brain so
2
+ // they travel with the project in git. They have no automatic consumer — agents
3
+ // read them from their own dirs — so `wendkeep sync-defs` copies them there:
4
+ // .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
5
+ // .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
6
+ // .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
7
+ // cross-platform robustness.
8
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
9
+ import { isAbsolute, join, resolve } from 'node:path';
10
+
11
+ export function syncDefs(vaultBase, projectPath) {
12
+ const out = { agents: [], skills: [] };
13
+
14
+ const agentsSrc = join(vaultBase, '.brain', 'agents');
15
+ if (existsSync(agentsSrc)) {
16
+ const dest = join(projectPath, '.codex', 'agents');
17
+ for (const f of readdirSync(agentsSrc)) {
18
+ if (!f.endsWith('.toml')) continue; // README.md etc. are docs, not defs
19
+ mkdirSync(dest, { recursive: true });
20
+ copyFileSync(join(agentsSrc, f), join(dest, f));
21
+ out.agents.push(f);
22
+ }
23
+ }
24
+
25
+ const skillsSrc = join(vaultBase, '.brain', 'skills');
26
+ if (existsSync(skillsSrc)) {
27
+ for (const name of readdirSync(skillsSrc)) {
28
+ const dir = join(skillsSrc, name);
29
+ if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
30
+ const dest = join(projectPath, '.claude', 'skills', name);
31
+ mkdirSync(dest, { recursive: true });
32
+ cpSync(dir, dest, { recursive: true });
33
+ out.skills.push(name);
34
+ }
35
+ }
36
+
37
+ return out;
38
+ }
39
+
40
+ // CLI entry for `wendkeep sync-defs`.
41
+ export function runSyncDefs(argv) {
42
+ let vault;
43
+ let project;
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const a = argv[i];
46
+ if (a === '--vault') vault = argv[++i];
47
+ else if (a.startsWith('--vault=')) vault = a.slice(8);
48
+ else if (a === '--project') project = argv[++i];
49
+ else if (a.startsWith('--project=')) project = a.slice(10);
50
+ }
51
+ const base = vault || process.env.OBSIDIAN_VAULT_PATH;
52
+ if (!base) {
53
+ process.stderr.write('wendkeep sync-defs: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
54
+ process.exit(2);
55
+ }
56
+ const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
57
+ const projectPath = resolve(project || process.cwd());
58
+ const r = syncDefs(vaultBase, projectPath);
59
+ process.stdout.write(
60
+ `wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
61
+ );
62
+ if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
63
+ if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
64
+ process.exit(0);
65
+ }
66
+
67
+ // --- seeding (init) ---------------------------------------------------------
68
+
69
+ const AGENTS_README = `# .brain/agents — versioned custom agent definitions
70
+
71
+ Canonical, versioned source for your project's custom agents (Codex \`.toml\` format).
72
+ \`wendkeep sync-defs\` copies each \`*.toml\` here into \`<project>/.codex/agents/\` so the
73
+ agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
74
+ `;
75
+
76
+ const EXAMPLE_AGENT = `# Example custom agent (Codex format). Replace with your own.
77
+ name = "example-agent"
78
+ description = "An example custom agent. Describe when to use it."
79
+ developer_instructions = "You are an example agent. State the role, rules, and output format here."
80
+ nickname_candidates = [ "example" ]
81
+ model = "gpt-5.5"
82
+ `;
83
+
84
+ const SKILLS_README = `# .brain/skills — versioned custom skill definitions
85
+
86
+ Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
87
+ \`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` so
88
+ the agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
89
+ `;
90
+
91
+ const EXAMPLE_SKILL = `---
92
+ name: example-skill
93
+ description: An example custom skill. Replace with your own — describe the trigger here.
94
+ ---
95
+ Use this skill when ... (describe when it applies).
96
+
97
+ Rules:
98
+ - (what the skill should do)
99
+ `;
100
+
101
+ function writeIfAbsent(path, content, created) {
102
+ if (!existsSync(path)) {
103
+ writeFileSync(path, content, 'utf8');
104
+ created.push(path);
105
+ }
106
+ }
107
+
108
+ // Seed the definitions layer in the vault's .brain (folders + README + one example
109
+ // each). Non-destructive. Returns the list of paths created.
110
+ export function seedDefinitions(brainDir) {
111
+ const created = [];
112
+ const agentsDir = join(brainDir, 'agents');
113
+ mkdirSync(agentsDir, { recursive: true });
114
+ writeIfAbsent(join(agentsDir, 'README.md'), AGENTS_README, created);
115
+ writeIfAbsent(join(agentsDir, 'example-agent.toml'), EXAMPLE_AGENT, created);
116
+
117
+ const exampleSkillDir = join(brainDir, 'skills', 'example-skill');
118
+ mkdirSync(exampleSkillDir, { recursive: true });
119
+ writeIfAbsent(join(brainDir, 'skills', 'README.md'), SKILLS_README, created);
120
+ writeIfAbsent(join(exampleSkillDir, 'SKILL.md'), EXAMPLE_SKILL, created);
121
+
122
+ return created;
123
+ }
package/src/taxonomy.mjs CHANGED
@@ -110,11 +110,8 @@ export const COMPANIONS = [
110
110
  default: false,
111
111
  marketplace: { source: 'git', url: 'https://github.com/JuliusBrussee/caveman.git' },
112
112
  plugin: 'caveman',
113
- // No MCP, no agnostic hook: on non-Claude agents, run its own installer.
114
- installer: {
115
- sh: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh',
116
- ps1: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1',
117
- },
113
+ // No MCP, no agnostic hook: on non-Claude agents, run its own installer
114
+ // (see cavemanInstallCommand — npx, non-interactive, Gemini excluded).
118
115
  },
119
116
  ];
120
117
 
@@ -178,21 +175,18 @@ export function companionHookSpecs(ids) {
178
175
  return specs;
179
176
  }
180
177
 
181
- // The caveman installer URL for this platform (.ps1 on Windows, .sh elsewhere).
182
- export function cavemanInstallerUrl(platform = process.platform) {
183
- const { sh, ps1 } = COMPANION_BY_ID.caveman.installer;
184
- return platform === 'win32' ? ps1 : sh;
185
- }
178
+ // Agents wendkeep installs caveman into via its cross-agent installer. Gemini is
179
+ // excluded on purpose: its CLI rejects caveman's agent tool names and crashes
180
+ // (libuv assertion) mid-install. caveman has no --exclude flag — only an --only
181
+ // allow-list so we enumerate the agents we want by their stable slugs.
182
+ export const CAVEMAN_AGENTS = ['claude', 'codex', 'cursor', 'copilot', 'amp', 'antigravity'];
186
183
 
187
- // Command to run caveman's installer FROM A LOCAL FILE. We download the script to a
188
- // temp file and run it as a file (not `irm|iex` / `curl|bash`) so the script gets a
189
- // real $PSCommandPath/$0 piping to iex leaves that null and the installer aborts
190
- // ("cannot bind argument to parameter 'Path'"). Pure: returns {command, args}.
191
- export function cavemanInstallerCommand(platform = process.platform, scriptPath) {
192
- if (platform === 'win32') {
193
- return { command: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath] };
194
- }
195
- return { command: 'bash', args: [scriptPath] };
184
+ // Non-interactive command installing caveman to CAVEMAN_AGENTS (never Gemini). Runs
185
+ // the published installer directly via npx (no install.ps1, so no $PSCommandPath
186
+ // issue). Returned as a shell string; the caller spawns it with shell: true.
187
+ export function cavemanInstallCommand(agents = CAVEMAN_AGENTS) {
188
+ const only = agents.flatMap((a) => ['--only', a]).join(' ');
189
+ return `npx -y github:JuliusBrussee/caveman -- --non-interactive ${only}`;
196
190
  }
197
191
 
198
192
  // Derive the default vault folder name from the project folder: `.<project>-vault`.
@@ -0,0 +1,156 @@
1
+ // Memory-compaction protocol for the curated .brain/CORE.md layer.
2
+ // Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
3
+ // - cap 25 lines (hard), 22 (soft warning) — 1 durable item per line
4
+ // - 3 required sections
5
+ // - no secrets / no real-provider PII emails
6
+ // Plus the seeded skeleton and the protocol reference doc.
7
+
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { isAbsolute, join, resolve } from 'node:path';
10
+
11
+ const HARD_LIMIT = 25;
12
+ const SOFT_LIMIT = 22;
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
+ ];
19
+
20
+ // Secret patterns reject only "real" values (length floor); abstract mentions like
21
+ // `sk_*` / `whsec_*` (trailing asterisk) are allowed.
22
+ const SECRET_PATTERNS = [
23
+ { name: 'Stripe secret key', regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/ },
24
+ { name: 'Stripe webhook secret', regex: /\bwhsec_[A-Za-z0-9]{20,}\b/ },
25
+ { name: 'JWT token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
26
+ { name: 'Bearer token', regex: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i },
27
+ { name: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{40,}\b/ },
28
+ { name: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/ },
29
+ { name: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
30
+ ];
31
+
32
+ const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
33
+
34
+ // Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
35
+ export function validateCore(content) {
36
+ const text = String(content ?? '');
37
+ const lines = text.split('\n');
38
+ const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
39
+ const errors = [];
40
+
41
+ if (lineCount > HARD_LIMIT) {
42
+ errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
43
+ }
44
+ for (const { label, regex } of REQUIRED_SECTIONS) {
45
+ if (!regex.test(text)) errors.push(`Seção obrigatória ausente: ## ${label}`);
46
+ }
47
+ for (const { name, regex } of SECRET_PATTERNS) {
48
+ const m = text.match(regex);
49
+ if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
50
+ }
51
+ const em = text.match(PII_EMAIL_REGEX);
52
+ if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
53
+
54
+ const warnings = [];
55
+ if (lineCount >= SOFT_LIMIT && lineCount <= HARD_LIMIT) {
56
+ warnings.push(`Tamanho ${lineCount}/${HARD_LIMIT} linhas — perto do limite; remover itens resolvidos (≥${SOFT_LIMIT}).`);
57
+ }
58
+
59
+ return { ok: errors.length === 0, errors, warnings, lineCount };
60
+ }
61
+
62
+ // The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
63
+ // curated hot layer exists with the right shape from day one.
64
+ export function renderCoreSkeleton() {
65
+ return `# CORE — núcleo curado da memória (.brain)
66
+
67
+ > 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>.
68
+
69
+ ## Preferências do Usuário
70
+ - (preferências duráveis: idioma, estilo, convenções)
71
+
72
+ ## Padrões Ativos
73
+ - (padrões/arquitetura ativos que outro agente precise saber)
74
+
75
+ ## Pendências Abertas
76
+ - (pendências/decisões em aberto — remova quando resolvidas)
77
+ `;
78
+ }
79
+
80
+ // The compaction-protocol reference doc dropped into the vault.
81
+ export function renderCompactionProtocol() {
82
+ return `# Protocolo de Memória — núcleo curado + digest automático (.brain)
83
+
84
+ > Como cada agente recebe, consulta e persiste memória entre sessões no seu vault.
85
+
86
+ ## 1. Duas camadas
87
+
88
+ - **QUENTE** (auto-injetada por sessão, budget ~45 linhas):
89
+ - \`.brain/CORE.md\` — curado à mão, **≤25 linhas** (1 item/linha): preferências, padrões, pendências.
90
+ - \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
91
+ - **FRIA** (sob demanda):
92
+ - \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
93
+ - Vault: \`02-Sessões/**\`, \`04-Decisões/**\`, \`05-Bugs/**\`, \`06-Aprendizados/**\`. Desce via \`/brain-recall <tópico>\`.
94
+
95
+ ## 2. Compactação = regra de geração (sem trabalho manual)
96
+
97
+ - **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
98
+ - **CORE**: quando ≥22 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
99
+
100
+ ## 3. O que escrever no CORE
101
+
102
+ Só estado **durável** que outro agente precise saber — preferência, padrão ativo, pendência aberta. 1 linha por item. Nunca log de sessão (isso é automático no vault).
103
+
104
+ 3 seções fixas (obrigatórias): \`## Preferências do Usuário\`, \`## Padrões Ativos\`, \`## Pendências Abertas\`.
105
+
106
+ ## 4. Sem segredos / PII
107
+
108
+ \`CORE.md\` nunca contém tokens (\`sk_*\`, \`whsec_*\`, JWT, Bearer), API keys, senhas ou email/telefone real. Use \`[REDACTED_SECRET]\` / \`user@example.com\`.
109
+
110
+ ## 5. Validação
111
+
112
+ \`\`\`bash
113
+ wendkeep validate-memory # valida <vault>/.brain/CORE.md
114
+ wendkeep validate-memory <path> # valida outro arquivo
115
+ \`\`\`
116
+
117
+ Checa: cap 25 (soft 22), 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
118
+ `;
119
+ }
120
+
121
+ // CLI entry for `wendkeep validate-memory [path]`. Resolves the target from an
122
+ // explicit path, else <vault>/.brain/CORE.md (--vault or OBSIDIAN_VAULT_PATH).
123
+ export function runValidateMemory(argv) {
124
+ let target;
125
+ let vault;
126
+ for (let i = 0; i < argv.length; i += 1) {
127
+ const a = argv[i];
128
+ if (a === '--vault') vault = argv[++i];
129
+ else if (a.startsWith('--vault=')) vault = a.slice(8);
130
+ else if (!a.startsWith('-')) target = a;
131
+ }
132
+ if (!target) {
133
+ const base = vault || process.env.OBSIDIAN_VAULT_PATH;
134
+ if (!base) {
135
+ process.stderr.write('wendkeep validate-memory: no target. Pass a path, --vault <path>, or set OBSIDIAN_VAULT_PATH.\n');
136
+ process.exit(2);
137
+ }
138
+ target = join(base, '.brain', 'CORE.md');
139
+ }
140
+ const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
141
+ if (!existsSync(abs)) {
142
+ process.stderr.write(`wendkeep validate-memory: not found: ${abs}\n`);
143
+ process.exit(2);
144
+ }
145
+ const res = validateCore(readFileSync(abs, 'utf8'));
146
+ if (!res.ok) {
147
+ process.stderr.write(`❌ CORE.md viola protocolo (${res.errors.length} erro${res.errors.length > 1 ? 's' : ''}):\n`);
148
+ for (const e of res.errors) process.stderr.write(` - ${e}\n`);
149
+ process.stderr.write('\nProtocolo: .brain/COMPACTION_PROTOCOL.md\n');
150
+ process.exit(1);
151
+ }
152
+ let msg = `✅ CORE.md OK (${res.lineCount} linhas, 3/3 seções, sem segredos).`;
153
+ for (const w of res.warnings) msg += `\n ⚠ ${w}`;
154
+ process.stdout.write(`${msg}\n`);
155
+ process.exit(0);
156
+ }