wendkeep 0.65.0 → 0.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.66.1] — 2026-07-29
8
+
9
+ ### Fixed
10
+
11
+ - **A curadoria de candidates agora é durável e idempotente.** `memory promote` e
12
+ `memory reject` registram decisões causais no ledger; repair/replay não recriam o conflito
13
+ resolvido, retries não duplicam eventos, candidates sobrepostos permanecem isolados e a
14
+ promoção recupera o checkpoint divergente do attempt correspondente sem tocar attempt novo.
15
+ - **Promoção de conflito exige escolha explícita.** `memory promote <candidate> --event
16
+ <event-id>` publica exatamente o evento escolhido, rejeita IDs externos ao candidate e não
17
+ permite que `blocked_by_core` sobrescreva a autoridade canônica de CORE.
18
+
19
+ ## [0.66.0] — 2026-07-29
20
+
21
+ ### Added
22
+
23
+ - **O workspace privado Integrations passa a ter um kernel canônico para Claude Code e Codex.**
24
+ Catálogo/projeção de hooks, envelope e provider, filtros de conteúdo, normalização de uso,
25
+ parsers de transcript e identidade de sessão/turno agora vivem em módulos puros sob
26
+ `packages/integrations/src/`, sem filesystem, ambiente global, Vault ou registry no import.
27
+ - **O tarball instalado prova a fronteira completa fora do checkout.** O teste executa `init` e
28
+ um hook `session-ensure` em consumidor temporário, valida estado persistido e as projeções
29
+ Claude/Codex contra o kernel empacotado, e confirma que Integrations permanece interno à
30
+ única publicação `wendkeep`.
31
+
32
+ ### Changed
33
+
34
+ - **As fachadas históricas agora injetam efeitos no kernel de Integrations.** Taxonomia e hooks
35
+ preservam assinaturas e identidade dos exports enquanto stdin/stdout, `process.env`, leitura de
36
+ transcripts, acesso ao Vault e ao registry continuam nas bordas; paths, schemas, configs e
37
+ sessões existentes não exigem migração.
38
+ - **Os workspaces privados deixaram de vazar por deep imports do wildcard raiz.** A allowlist de
39
+ exports mantém `wendkeep/harness`, `wendkeep/vault` e os caminhos históricos publicados, mas
40
+ bloqueia `packages/*`, inclusive variantes percent-encoded; MCP e Integrations seguem como
41
+ adapters irmãos sem dependência direta.
42
+
43
+ ### Fixed
44
+
45
+ - **A reconexão de sessão por transcript agora respeita o provider.** Uma entrada do registry de
46
+ outro host é ignorada sem substituir uma identidade canônica válida já inspecionada.
47
+ - **A suíte integral mantém determinismo sob carga de I/O no Windows.** O runner limita a duas as
48
+ files executadas em paralelo, preservando a concorrência multiprocesso dentro dos testes de
49
+ CAS/locks e todos os asserts, sem falsos vermelhos causados por starvation entre suítes.
50
+
7
51
  ## [0.65.0] — 2026-07-29
8
52
 
9
53
  ### Added
package/README.en.md CHANGED
@@ -17,14 +17,24 @@
17
17
 
18
18
  The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
19
19
  `integrations`, and `pi` — without fragmenting installation. The private `cli`, `harness`,
20
- `vault`, and `mcp` workspaces now canonically own the executable runtime, Operating Profiles/the
21
- sensor engine, safe binding/the Shared Project Memory v2 kernel, and the MCP configuration kernel,
22
- respectively. The root package exposes Harness and Vault through `wendkeep/harness` and
23
- `wendkeep/vault`; CLI and MCP remain private surfaces, reached only through the binaries and the
24
- configuration effects of `init`. Historical imports keep working through compatibility facades
20
+ `vault`, `mcp`, and `integrations` workspaces now canonically own the executable runtime,
21
+ Operating Profiles/the sensor engine, safe binding/the Shared Project Memory v2 kernel, the MCP
22
+ configuration kernel, and pure Claude/Codex integration rules, respectively. The root package
23
+ exposes Harness and Vault through `wendkeep/harness` and `wendkeep/vault`; CLI, MCP, and
24
+ Integrations remain private surfaces, reached only through the binaries, the configuration effects
25
+ of `init`, and historical facades. Historical imports keep working through compatibility facades
25
26
  and no session data needs migration;
26
27
  see the [modular architecture](docs/en/architecture.md).
27
28
 
29
+ In the **0.66 Integrations Kernel** phase, `packages/integrations/src/` becomes the canonical
30
+ authority for the hook catalog and projection, the envelope/provider, transcript content and usage
31
+ filters and parsers, and session identity. These rules are pure: stdin/stdout, environment,
32
+ filesystem, Vault, and registry effects remain in the historical facades. MCP and Integrations are
33
+ sibling adapters with no dependency between them, and the direction remains
34
+ `cli/mcp/integrations/pi → Harness → Vault`. Hooks, sessions, paths, configuration, and schemas
35
+ remain equivalent; the private `@wendkeep/integrations` workspace stays inside the single
36
+ published `wendkeep` package, with no public `wendkeep/integrations` subpath. Pi is the next phase.
37
+
28
38
  In the **0.65 MCP Configuration Kernel** phase, `packages/mcp/src/config.mjs` becomes the
29
39
  canonical authority for the MCPVault entry, catalog-described server selection, and `.mcp.json`
30
40
  merging. `src/taxonomy.mjs` supplies descriptors while `src/init.mjs` retains only filesystem
@@ -284,7 +294,10 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
284
294
  ambiguity uses `memory reconcile <session> --by-session <successor>
285
295
  --reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
286
296
  without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
287
- explicit curation with `memory promote <id>` or `memory reject <id>`; doctor only diagnoses.
297
+ explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
298
+ candidate, while `memory reject <id>` keeps the current value. Decisions are idempotent and
299
+ survive repair/replay; `blocked_by_core` cannot override CORE. Doctor only diagnoses. See
300
+ [memory and curation](docs/en/commands/memory.md).
288
301
 
289
302
  Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
290
303
 
package/README.md CHANGED
@@ -17,14 +17,24 @@
17
17
 
18
18
  The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
19
19
  `integrations`, and `pi` — without fragmenting installation. The private `cli`, `harness`,
20
- `vault`, and `mcp` workspaces now canonically own the executable runtime, Operating Profiles/the
21
- sensor engine, safe binding/the Shared Project Memory v2 kernel, and the MCP configuration kernel,
22
- respectively. The root package exposes Harness and Vault through `wendkeep/harness` and
23
- `wendkeep/vault`; CLI and MCP remain private surfaces, reached only through the binaries and the
24
- configuration effects of `init`. Historical imports keep working through compatibility facades
20
+ `vault`, `mcp`, and `integrations` workspaces now canonically own the executable runtime,
21
+ Operating Profiles/the sensor engine, safe binding/the Shared Project Memory v2 kernel, the MCP
22
+ configuration kernel, and pure Claude/Codex integration rules, respectively. The root package
23
+ exposes Harness and Vault through `wendkeep/harness` and `wendkeep/vault`; CLI, MCP, and
24
+ Integrations remain private surfaces, reached only through the binaries, the configuration effects
25
+ of `init`, and historical facades. Historical imports keep working through compatibility facades
25
26
  and no session data needs migration;
26
27
  see the [modular architecture](docs/en/architecture.md).
27
28
 
29
+ In the **0.66 Integrations Kernel** phase, `packages/integrations/src/` becomes the canonical
30
+ authority for the hook catalog and projection, the envelope/provider, transcript content and usage
31
+ filters and parsers, and session identity. These rules are pure: stdin/stdout, environment,
32
+ filesystem, Vault, and registry effects remain in the historical facades. MCP and Integrations are
33
+ sibling adapters with no dependency between them, and the direction remains
34
+ `cli/mcp/integrations/pi → Harness → Vault`. Hooks, sessions, paths, configuration, and schemas
35
+ remain equivalent; the private `@wendkeep/integrations` workspace stays inside the single
36
+ published `wendkeep` package, with no public `wendkeep/integrations` subpath. Pi is the next phase.
37
+
28
38
  In the **0.65 MCP Configuration Kernel** phase, `packages/mcp/src/config.mjs` becomes the
29
39
  canonical authority for the MCPVault entry, catalog-described server selection, and `.mcp.json`
30
40
  merging. `src/taxonomy.mjs` supplies descriptors while `src/init.mjs` retains only filesystem
@@ -284,7 +294,10 @@ backup/audit; divergent mirrors fail closed. A demonstrably superseded
284
294
  ambiguity uses `memory reconcile <session> --by-session <successor>
285
295
  --reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
286
296
  without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
287
- explicit curation with `memory promote <id>` or `memory reject <id>`; doctor only diagnoses.
297
+ explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
298
+ candidate, while `memory reject <id>` keeps the current value. Decisions are idempotent and
299
+ survive repair/replay; `blocked_by_core` cannot override CORE. Doctor only diagnoses. See
300
+ [memory and curation](docs/en/commands/memory.md).
288
301
 
289
302
  Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
290
303
 
@@ -78,6 +78,14 @@ When MCP is enabled, `init` preserves existing properties and servers in `.mcp.j
78
78
  and the reconciled proposal is written to `.mcp.json.new`. Since version 0.65, this composition is
79
79
  owned by the private MCP kernel without changing commands, flags, or the public npm surface.
80
80
 
81
+ Pure rules that project Claude/Codex hooks and interpret envelopes, transcripts, usage, and
82
+ identity belong to the private `@wendkeep/integrations` workspace. Historical facades retain
83
+ stdin/stdout, environment, filesystem, Vault, and registry effects. This adds no commands or flags,
84
+ changes neither hooks nor sessions, and requires no Vault, configuration, path, or schema migration.
85
+ MCP and Integrations remain sibling adapters with no dependency between them, and the direction
86
+ remains `cli/mcp/integrations/pi → Harness → Vault`. Everything stays inside the single published
87
+ `wendkeep` package; there is no public `wendkeep/integrations` surface. Pi is the next modular phase.
88
+
81
89
  ## Common errors and diagnosis
82
90
 
83
91
  - Wrong vault: inspect `.wendkeep.json` and run `wendkeep doctor --vault <path>`.
@@ -64,7 +64,8 @@ to revision 1. Replaying that prompt or Stop does not duplicate the event/revisi
64
64
  - Partial/corrupt v2 bundle: use status and repair; migration is not a corruption tool.
65
65
  - First post-migration Stop is `ambiguous`: verify that its `turn_id` belongs to the transcript and
66
66
  that `UserPromptSubmit` opened/advanced the recovery activation.
67
- - Many candidates: curate gradually with `memory promote`/`memory reject`.
67
+ - Many candidates: curate gradually with `memory promote`/`memory reject`; conflicts require
68
+ `memory promote <candidate> --event <event-id>` to choose the winner explicitly.
68
69
  - Legacy warning remains after apply: verify the selected vault and project binding.
69
70
 
70
71
  ## Next steps
@@ -26,7 +26,7 @@ Pass the vault explicitly in automation. Preserve backups and evidence before re
26
26
  npx wendkeep memory status [--gate] --vault <vault>
27
27
  npx wendkeep memory repair --vault <vault>
28
28
  npx wendkeep memory reconcile <ambiguous-session> --by-session <successor-session> --reason <reason> [--apply] --vault <vault>
29
- npx wendkeep memory promote <candidate> --vault <vault>
29
+ npx wendkeep memory promote <candidate> [--event <event-id>] --vault <vault>
30
30
  npx wendkeep memory reject <candidate> --vault <vault>
31
31
  npx wendkeep validate-memory [CORE-path]
32
32
  npx wendkeep validate-memory --vault <v2-vault>
@@ -63,7 +63,14 @@ npx wendkeep validate-memory --vault <v2-vault>
63
63
  Junctions, symlinks, reparse points, or hardlinks fail closed without touching external bytes.
64
64
  Locks publish owner and lease atomically, never reap a live PID by age alone, and release only
65
65
  the lease they acquired.
66
- - `promote`/`reject` append auditable decisions and never rewrite the ledger in place.
66
+ - `promote`/`reject` append an auditable, idempotent decision to the ledger. Replay and repair
67
+ preserve that decision and do not recreate the resolved candidate. For a `conflict` candidate,
68
+ `promote` requires an `--event <event-id>` that belongs to the candidate; date or random ID
69
+ never picks an implicit winner. `reject` preserves the current operational value. A
70
+ `blocked_by_core` candidate can only be rejected: promotion first requires canonical CORE
71
+ curation. If the selected event still belongs to the matching latest `projected` attempt,
72
+ promotion also refreshes its checkpoint and mirror causally; JSON reports
73
+ `checkpointRefreshed`, and a newer concurrent attempt remains untouched.
67
74
  - `validate-memory <CORE.md>` checks the 25-line cap, required sections, and secrets.
68
75
  - `validate-memory --vault` requires a complete v2 bundle and is not the legacy-vault gate.
69
76
 
@@ -74,7 +81,8 @@ npx wendkeep memory status --gate --vault .MyApp-vault
74
81
  npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --vault .MyApp-vault
75
82
  npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --apply --vault .MyApp-vault
76
83
  npx wendkeep validate-memory .MyApp-vault/.brain/CORE.md
77
- npx wendkeep memory promote candidate-123 --vault .MyApp-vault
84
+ npx wendkeep memory promote candidate-123 --event mem-selected --vault .MyApp-vault
85
+ npx wendkeep memory reject candidate-456 --vault .MyApp-vault
78
86
  ```
79
87
 
80
88
  ## Expected result
@@ -97,6 +105,9 @@ of a global projection that has already advanced with concurrent events.
97
105
  `memory reconcile` dry run before authorizing `--apply`; the command fails when the ambiguous
98
106
  attempt already contains event IDs.
99
107
  - Ordinary pending candidate: recoverable warning, requiring human choice when appropriate.
108
+ - `promote` reports that `--event` is required: inspect the candidate `event_ids`, compare their
109
+ provenance/value, and name the winner explicitly. An ID outside the candidate fails without
110
+ mutating the ledger or projections.
100
111
  - Missing `event_cursor` or mismatched v2 hash: preserve the bundle and assess `memory repair`.
101
112
  - `validate-memory --vault` fails on legacy: validate CORE only or migrate first.
102
113
 
@@ -79,6 +79,14 @@ e adiciona `wendkeep-vault`. Se o JSON existente for inválido, o arquivo origin
79
79
  a byte intacto e a proposta reconciliada é gravada em `.mcp.json.new`. Desde a versão 0.65, essa
80
80
  composição pertence ao kernel MCP privado, sem alterar comandos, flags ou a superfície npm pública.
81
81
 
82
+ As regras puras que projetam os hooks de Claude/Codex e interpretam envelopes, transcripts, uso e
83
+ identidade pertencem ao workspace privado `@wendkeep/integrations`. As fachadas históricas mantêm
84
+ os efeitos de stdin/stdout, ambiente, filesystem, Vault e registry. Isso não acrescenta comandos ou
85
+ flags, não altera hooks ou sessões e não exige migração de cofre, config, paths ou schemas. MCP e
86
+ Integrations permanecem adapters irmãos sem dependência, e a direção continua
87
+ `cli/mcp/integrations/pi → Harness → Vault`. Tudo permanece dentro do único pacote publicado
88
+ `wendkeep`; não existe `wendkeep/integrations` público. A próxima fase modular é Pi.
89
+
82
90
  ## Erros comuns e diagnóstico
83
91
 
84
92
  - Cofre errado: confira `.wendkeep.json` e rode `wendkeep doctor --vault <path>`.
@@ -64,7 +64,8 @@ revision 1. Repetir esse prompt ou Stop não duplica evento/revision.
64
64
  - Bundle v2 parcial/corrompido: use status e repair; migração não é ferramenta de corrupção.
65
65
  - Primeiro Stop pós-migração fica `ambiguous`: confirme que o `turn_id` pertence ao transcript e
66
66
  que `UserPromptSubmit` abriu/avançou a activation de recuperação.
67
- - Candidates numerosos: curate gradualmente com `memory promote`/`memory reject`.
67
+ - Candidates numerosos: curate gradualmente com `memory promote`/`memory reject`; conflitos
68
+ exigem `memory promote <candidate> --event <event-id>` para escolher o vencedor explicitamente.
68
69
  - Warning legado após apply: confirme o vault efetivamente selecionado e o vínculo do projeto.
69
70
 
70
71
  ## Próximos passos
@@ -26,7 +26,7 @@ Informe o vault explicitamente em automações. Preserve backups e evidências a
26
26
  npx wendkeep memory status [--gate] --vault <cofre>
27
27
  npx wendkeep memory repair --vault <cofre>
28
28
  npx wendkeep memory reconcile <sessão-ambígua> --by-session <sessão-sucessora> --reason <motivo> [--apply] --vault <cofre>
29
- npx wendkeep memory promote <candidate> --vault <cofre>
29
+ npx wendkeep memory promote <candidate> [--event <event-id>] --vault <cofre>
30
30
  npx wendkeep memory reject <candidate> --vault <cofre>
31
31
  npx wendkeep validate-memory [caminho-do-CORE]
32
32
  npx wendkeep validate-memory --vault <cofre-v2>
@@ -61,7 +61,13 @@ npx wendkeep validate-memory --vault <cofre-v2>
61
61
  candidates, registry, notas, backups, temporários e sidecars antes de ler ou escrever. Junction,
62
62
  symlink, reparse point ou hardlink falham fechados sem tocar bytes externos. Locks publicam owner
63
63
  e lease atomicamente, não colhem PID vivo apenas por idade e só liberam a lease adquirida.
64
- - `promote`/`reject` acrescentam decisão auditável; nunca reescrevem o ledger no lugar.
64
+ - `promote`/`reject` acrescentam uma decisão auditável e idempotente ao ledger. Replay e repair
65
+ preservam a decisão e não recriam o candidate resolvido. Para candidate `conflict`, `promote`
66
+ exige `--event <event-id>` pertencente ao candidate; não há vencedor implícito por data ou ID.
67
+ `reject` preserva o valor operacional atual. Candidate `blocked_by_core` só pode ser rejeitado:
68
+ promover exige antes alterar CORE pela curadoria canônica. Se o evento escolhido ainda pertence
69
+ ao último attempt `projected` correspondente, a promoção também atualiza causalmente checkpoint
70
+ e espelho; o JSON retorna `checkpointRefreshed`, e um attempt concorrente mais novo não é tocado.
65
71
  - `validate-memory <CORE.md>` valida cap de 25 linhas, seções e segredos.
66
72
  - `validate-memory --vault` exige bundle v2 completo; não é o gate correto para vault legado.
67
73
 
@@ -72,7 +78,8 @@ npx wendkeep memory status --gate --vault .MeuApp-vault
72
78
  npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --vault .MeuApp-vault
73
79
  npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --apply --vault .MeuApp-vault
74
80
  npx wendkeep validate-memory .MeuApp-vault/.brain/CORE.md
75
- npx wendkeep memory promote candidate-123 --vault .MeuApp-vault
81
+ npx wendkeep memory promote candidate-123 --event mem-escolhido --vault .MeuApp-vault
82
+ npx wendkeep memory reject candidate-456 --vault .MeuApp-vault
76
83
  ```
77
84
 
78
85
  ## Resultado esperado
@@ -94,6 +101,9 @@ prefixo válido de uma projeção global que já avançou com eventos concorrent
94
101
  ambiguidade for comprovadamente substituída por uma sessão sucessora, revise o dry-run de
95
102
  `memory reconcile` antes de autorizar `--apply`; o comando falha se o attempt ambíguo tiver IDs.
96
103
  - Candidate pendente comum: warning recuperável, exige decisão humana quando apropriado.
104
+ - `promote` informa que `--event` é obrigatório: leia os `event_ids` do candidate, compare a
105
+ proveniência/valor e indique explicitamente o vencedor. ID que não pertence ao candidate falha
106
+ sem mutar ledger ou projeções.
97
107
  - `event_cursor` ausente ou hash divergente em v2: preserve o bundle e avalie `memory repair`.
98
108
  - `validate-memory --vault` falha no legado: valide apenas CORE ou migre primeiro.
99
109
 
@@ -1,159 +1,159 @@
1
- // .agent/hooks/brain-core.mjs
2
- // Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
3
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { basename, join } from 'node:path';
5
- import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
6
- import { getLocale } from './locale.mjs';
7
-
8
- export function brainDir(vaultBase) {
9
- return join(vaultBase, '.brain');
10
- }
11
-
12
- // Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
13
- export function parseFrontmatter(content) {
14
- const m = content.match(/^---\n([\s\S]*?)\n---/);
15
- if (!m) return {};
16
- const data = {};
17
- const lines = m[1].split('\n');
18
- for (let i = 0; i < lines.length; i++) {
19
- const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
20
- if (!kv) continue;
21
- const key = kv[1];
22
- const val = kv[2];
23
- if (val === '') {
24
- const list = [];
25
- while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
26
- list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
27
- }
28
- data[key] = list.length ? list : '';
29
- } else {
30
- data[key] = stripYamlQuotes(val.trim());
31
- }
32
- }
33
- return data;
34
- }
35
-
36
- function walkMd(dir) {
37
- const out = [];
38
- let entries;
39
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
40
- for (const e of entries) {
41
- const fp = join(dir, e.name);
42
- if (e.isDirectory()) out.push(...walkMd(fp));
43
- else if (e.name.endsWith('.md')) out.push(fp);
44
- }
45
- return out;
46
- }
47
-
48
- const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
49
- function derivedLinks(content) {
50
- const dec = new Set(), bug = new Set(), lea = new Set();
51
- let m;
52
- while ((m = DERIVED_RE.exec(content))) {
53
- const t = m[1];
54
- if (t.startsWith('04-')) dec.add(t);
55
- else if (t.startsWith('05-')) bug.add(t);
56
- else if (t.startsWith('06-')) lea.add(t);
57
- }
58
- return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
59
- }
60
-
61
- // Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
62
- export function buildBrainIndex(vaultBase) {
63
- const rows = [];
64
- for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
65
- let content;
66
- try { content = readFileSync(fp, 'utf8'); } catch { continue; }
67
- const fm = parseFrontmatter(content);
68
- if (fm.type && fm.type !== 'session') continue;
69
- const der = derivedLinks(content);
70
- rows.push({
71
- session_id: fm.session_id || '',
72
- date: fm.date || '',
73
- provider: fm.provider || '',
74
- status: fm.status || '',
75
- summary: fm.summary || '',
76
- file: toVaultRelative(vaultBase, fp),
77
- tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
78
- decisions: der.decisions,
79
- bugs: der.bugs,
80
- learnings: der.learnings,
81
- });
82
- }
83
- rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
84
- ensureDir(brainDir(vaultBase));
85
- const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
86
- writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
87
- return rows;
88
- }
89
-
90
- // Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
91
- export function loadIndex(vaultBase) {
92
- try {
93
- return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
94
- .split('\n').filter(Boolean).map((l) => JSON.parse(l));
95
- } catch {
96
- return [];
97
- }
98
- }
99
-
100
- const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
101
-
102
- function adrNumber(path) {
103
- const m = path.match(/ADR-(\d+)/);
104
- return m ? Number(m[1]) : -1;
105
- }
106
-
107
- // Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
108
- // Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
109
- export function buildBrainDigest(vaultBase, rows = null) {
110
- const data = rows ?? loadIndex(vaultBase);
111
- const byDateDesc = [...data].sort((a, b) =>
112
- String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
113
-
114
- const seen = new Set();
115
- const pick = (kind, max) => {
116
- const out = [];
117
- for (const r of byDateDesc) {
118
- for (const p of r[kind] || []) {
119
- if (out.length >= max) return out;
120
- if (!seen.has(p)) { seen.add(p); out.push(p); }
121
- }
122
- }
123
- return out;
124
- };
125
-
126
- // The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
127
- // context. Keep only targets that resolve to a real note (by vault-relative path or basename)
128
- // and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
129
- // caps still fill after filtering.
130
- const known = new Set();
131
- for (const r of data) {
132
- const rel = String(r.file || '').replace(/\.md$/i, '');
133
- if (rel) { known.add(rel); known.add(basename(rel)); }
134
- }
135
- const resolves = (p) => {
136
- const t = String(p || '').replace(/\.md$/i, '').trim();
137
- if (!t || t.includes('...') || t.includes('…')) return false;
138
- return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
139
- };
140
- const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
141
-
142
- const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
143
- const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
144
- const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
145
- const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
146
-
147
- const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
148
- for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
149
- for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
150
- for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
151
- for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
152
-
153
- const shown = sessions.length;
154
- if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
155
-
156
- ensureDir(brainDir(vaultBase));
157
- writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
158
- return lines;
159
- }
1
+ // .agent/hooks/brain-core.mjs
2
+ // Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
3
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { basename, join } from 'node:path';
5
+ import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
6
+ import { getLocale } from './locale.mjs';
7
+
8
+ export function brainDir(vaultBase) {
9
+ return join(vaultBase, '.brain');
10
+ }
11
+
12
+ // Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
13
+ export function parseFrontmatter(content) {
14
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
15
+ if (!m) return {};
16
+ const data = {};
17
+ const lines = m[1].split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
20
+ if (!kv) continue;
21
+ const key = kv[1];
22
+ const val = kv[2];
23
+ if (val === '') {
24
+ const list = [];
25
+ while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
26
+ list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
27
+ }
28
+ data[key] = list.length ? list : '';
29
+ } else {
30
+ data[key] = stripYamlQuotes(val.trim());
31
+ }
32
+ }
33
+ return data;
34
+ }
35
+
36
+ function walkMd(dir) {
37
+ const out = [];
38
+ let entries;
39
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
40
+ for (const e of entries) {
41
+ const fp = join(dir, e.name);
42
+ if (e.isDirectory()) out.push(...walkMd(fp));
43
+ else if (e.name.endsWith('.md')) out.push(fp);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
49
+ function derivedLinks(content) {
50
+ const dec = new Set(), bug = new Set(), lea = new Set();
51
+ let m;
52
+ while ((m = DERIVED_RE.exec(content))) {
53
+ const t = m[1];
54
+ if (t.startsWith('04-')) dec.add(t);
55
+ else if (t.startsWith('05-')) bug.add(t);
56
+ else if (t.startsWith('06-')) lea.add(t);
57
+ }
58
+ return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
59
+ }
60
+
61
+ // Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
62
+ export function buildBrainIndex(vaultBase) {
63
+ const rows = [];
64
+ for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
65
+ let content;
66
+ try { content = readFileSync(fp, 'utf8'); } catch { continue; }
67
+ const fm = parseFrontmatter(content);
68
+ if (fm.type && fm.type !== 'session') continue;
69
+ const der = derivedLinks(content);
70
+ rows.push({
71
+ session_id: fm.session_id || '',
72
+ date: fm.date || '',
73
+ provider: fm.provider || '',
74
+ status: fm.status || '',
75
+ summary: fm.summary || '',
76
+ file: toVaultRelative(vaultBase, fp),
77
+ tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
78
+ decisions: der.decisions,
79
+ bugs: der.bugs,
80
+ learnings: der.learnings,
81
+ });
82
+ }
83
+ rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
84
+ ensureDir(brainDir(vaultBase));
85
+ const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
86
+ writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
87
+ return rows;
88
+ }
89
+
90
+ // Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
91
+ export function loadIndex(vaultBase) {
92
+ try {
93
+ return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
94
+ .split('\n').filter(Boolean).map((l) => JSON.parse(l));
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
99
+
100
+ const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
101
+
102
+ function adrNumber(path) {
103
+ const m = path.match(/ADR-(\d+)/);
104
+ return m ? Number(m[1]) : -1;
105
+ }
106
+
107
+ // Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
108
+ // Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
109
+ export function buildBrainDigest(vaultBase, rows = null) {
110
+ const data = rows ?? loadIndex(vaultBase);
111
+ const byDateDesc = [...data].sort((a, b) =>
112
+ String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
113
+
114
+ const seen = new Set();
115
+ const pick = (kind, max) => {
116
+ const out = [];
117
+ for (const r of byDateDesc) {
118
+ for (const p of r[kind] || []) {
119
+ if (out.length >= max) return out;
120
+ if (!seen.has(p)) { seen.add(p); out.push(p); }
121
+ }
122
+ }
123
+ return out;
124
+ };
125
+
126
+ // The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
127
+ // context. Keep only targets that resolve to a real note (by vault-relative path or basename)
128
+ // and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
129
+ // caps still fill after filtering.
130
+ const known = new Set();
131
+ for (const r of data) {
132
+ const rel = String(r.file || '').replace(/\.md$/i, '');
133
+ if (rel) { known.add(rel); known.add(basename(rel)); }
134
+ }
135
+ const resolves = (p) => {
136
+ const t = String(p || '').replace(/\.md$/i, '').trim();
137
+ if (!t || t.includes('...') || t.includes('…')) return false;
138
+ return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
139
+ };
140
+ const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
141
+
142
+ const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
143
+ const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
144
+ const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
145
+ const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
146
+
147
+ const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
148
+ for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
149
+ for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
150
+ for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
151
+ for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
152
+
153
+ const shown = sessions.length;
154
+ if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
155
+
156
+ ensureDir(brainDir(vaultBase));
157
+ writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
158
+ return lines;
159
+ }