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 +44 -0
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/docs/en/commands/getting-started.md +8 -0
- package/docs/en/commands/memory-migration.md +2 -1
- package/docs/en/commands/memory.md +14 -3
- package/docs/pt-BR/commands/getting-started.md +8 -0
- package/docs/pt-BR/commands/memory-migration.md +2 -1
- package/docs/pt-BR/commands/memory.md +13 -3
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/obsidian-common.mjs +24 -124
- package/hooks/session-identity.mjs +21 -136
- package/hooks/session-stop.mjs +18 -384
- package/hooks/token-usage.mjs +12 -56
- package/package.json +13 -4
- package/packages/cli/src/index.mjs +1 -1
- package/packages/integrations/package.json +2 -1
- package/packages/integrations/src/hook-envelope.mjs +103 -0
- package/packages/integrations/src/host-hooks.mjs +80 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/prompt-content.mjs +20 -0
- package/packages/integrations/src/session-identity.mjs +171 -0
- package/packages/integrations/src/transcript-usage.mjs +53 -0
- package/packages/integrations/src/transcripts.mjs +423 -0
- package/packages/vault/src/memory-schema.mjs +25 -0
- package/packages/vault/src/memory-store.mjs +22 -2
- package/src/memory.mjs +177 -17
- package/src/taxonomy.mjs +24 -78
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 `
|
|
21
|
-
sensor engine, safe binding/the Shared Project Memory v2 kernel,
|
|
22
|
-
|
|
23
|
-
`wendkeep/vault`; CLI
|
|
24
|
-
|
|
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
|
|
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 `
|
|
21
|
-
sensor engine, safe binding/the Shared Project Memory v2 kernel,
|
|
22
|
-
|
|
23
|
-
`wendkeep/vault`; CLI
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/hooks/brain-core.mjs
CHANGED
|
@@ -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
|
+
}
|