wendkeep 0.57.2 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.58.0] — 2026-07-26
8
+
9
+ ### Added
10
+
11
+ - **Shared Project Memory v2 torna o handoff entre agentes causal, auditável e diretamente
12
+ injetável.** O `SessionStop` publica eventos locais imutáveis numa outbox, o projector
13
+ serializa o ledger `MEMORY_EVENTS.jsonl` e gera `SHARED_MEMORY.md`; o próximo
14
+ `startup|clear|compact` recebe CORE + SHARED completos antes do contexto da change. Epochs
15
+ de activation impedem um Stop atrasado de fechar ou promover sobre uma sessão mais nova.
16
+ - **Curadoria operacional ganhou comandos e gate próprios.** `wendkeep memory` oferece
17
+ `status`, migração dry-run/apply com backup, `repair` e decisões append-only de
18
+ `promote|reject`; `memory-health` bloqueia verify/archive em corrupção, lag ou conflito
19
+ ativo e mantém outbox/candidates comuns como avisos recuperáveis.
20
+
21
+ ### Security
22
+
23
+ - **Memória v2 permanece local-only e sanitizada em duas fronteiras.** Secrets, tokens, PII,
24
+ paths de transcript e payloads do harness são removidos antes da persistência e novamente
25
+ antes da injeção; eventos são isolados por `project_id`, CORE/SHARED nunca são truncados
26
+ silenciosamente e o envelope total possui budget rígido de 24 KiB.
27
+
7
28
  ## [0.57.2] — 2026-07-25
8
29
 
9
30
  ### Fixed
package/README.en.md CHANGED
@@ -38,7 +38,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
38
38
  |---|---|
39
39
  | **Capture** — every turn, on disk | `SessionStart` / `Stop` hooks write each session to a dated Markdown note: prompts, iterations, files touched, wikilinks. |
40
40
  | **Derive** — decisions, bugs, learnings | Pulled from the transcript into their own notes, backlinked to the session. Your history becomes navigable, not archival. |
41
- | **Recall** — injected back | A budget‑capped `CORE` + `DIGEST` and every open change are fed to the agent at the next `SessionStart`. It resumes where it left off. |
41
+ | **Recall** — injected back | Canonical `CORE` + operational `SHARED_MEMORY`, with explicit budgets, are injected on `startup`, `/clear`, and `/compact`; `DIGEST` remains a deep-recall index and legacy fallback. |
42
42
  | **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault. |
43
43
  | **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
44
44
  | **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
@@ -71,9 +71,9 @@ npx wendkeep init
71
71
 
72
72
  Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
73
73
  6. 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`. Re-apply it any time on an existing vault with `wendkeep theme sync` — Obsidian owns `graph.json` and can drop the color groups (a grey graph); the re-sync restores them without a full re-`init`.
74
- 7. 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).
74
+ 7. Seed **Shared Project Memory v2** without overwriting existing artifacts: `.brain/CORE.md` (hand-curated canonical truth), `.brain/SHARED_MEMORY.md` (generated operational state), `.brain/MEMORY_EVENTS.jsonl` (append-only ledger), `.brain/MEMORY_CANDIDATES.jsonl` (curation queue), and `.brain/COMPACTION_PROTOCOL.md`. The durable outbox appears under `.brain/memory-outbox/` when events exist; `DIGEST.md` and `index.jsonl` remain deep recall. Everything stays local to the vault.
75
75
  8. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify` (some ship templates — e.g. `wk-verify`'s `verdict-template.json` + reviewer prompt). `init` runs `wendkeep sync-defs` for you, delivering the skills to `.claude/skills/` and `.agents/skills/`, and the agent definitions (`.brain/agents/*.toml`) to `.codex/agents/`, plus a managed section in `AGENTS.md` that indexes the skills for Codex; `sync-defs --check` detects stale copies (re-run `sync-defs` after editing `.brain`).
76
- 9. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` — a critical `memory-validation` sensor (`npx wendkeep validate-memory`) plus one for each of `typecheck` / `test` / `lint` / `build` found in your `package.json`. Add your own with `wendkeep sensors add`. Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
76
+ 9. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` — critical memory validation/health sensors plus one for each of `typecheck` / `test` / `lint` / `build` found in your `package.json`. `memory-health` blocks delivery on corruption, projection divergence, or an active conflict; pending outbox events and ordinary candidates are warnings. Add sensors with `wendkeep sensors add`. Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
77
77
 
78
78
  ```bash
79
79
  npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (no companions unless you ask)
@@ -182,11 +182,45 @@ which the reseed never touches.
182
182
  | `wendkeep renumber-learnings` | Same for `06-Aprendizados`/`06-Learnings` → `APR-NNNN-<slug>`. |
183
183
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). `--change <slug>` ties the lesson to a change; `--vault P`. |
184
184
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift, `--reseed` refreshes the `wk-*` skills from the installed version's seeds. |
185
- | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
185
+ | `wendkeep memory status [--gate] --vault P` | Inspect the v2 bundle without mutating it. `--gate` exits 1 only for a blocking state; warnings keep exit code 0. |
186
+ | `wendkeep memory migrate [--apply] --vault P` | Convert a legacy `SHARED_MEMORY.md`. Dry-run by default; `--apply` creates a backup, turns legacy content into candidates, and publishes a valid v2 projection without editing CORE. |
187
+ | `wendkeep memory repair --vault P` | Repair a partial/corrupt ledger under lock, preserving the original bytes in a `.bak`, retaining valid events, and re-projecting state. |
188
+ | `wendkeep memory promote <candidate> --vault P` | Promote a candidate by ID by appending an auditable event; never edits the ledger in place. |
189
+ | `wendkeep memory reject <candidate> --vault P` | Reject a candidate by ID by appending the decision to the audit history. |
190
+ | `wendkeep validate-memory [path]` | Compatibility mode: validate `.brain/CORE.md` only (cap 25, 3 sections, no secrets/PII). Use `--vault <path>` to validate CORE + ledger + SHARED as a v2 bundle. |
186
191
  | `wendkeep theme sync [--vault P]` | Re-apply the colour system (CSS snippet + graph groups) to an existing vault — recovers a grey graph without re-running `init`. |
187
- | `wendkeep doctor [--vault P]` | Vault health check. Beyond session and registry integrity, it reports in sections: `[links]` (graph orphans), `[notas]` (stacked frontmatter), `[preços]` (a model missing from the price table, which would close the session at zero cost) and `[derivadas]` (stale body sections) each with its repair command alongside. |
192
+ | `wendkeep doctor [--vault P]` | Read-only vault health check. Beyond sessions/registry, links, notes, prices, and derived sections, it checks the v2 bundle and points to `memory status --gate` or `memory repair`; doctor never projects or repairs by itself. |
188
193
  | `wendkeep --version` / `--help` | Version / usage. |
189
194
 
195
+ ## Shared Project Memory v2
196
+
197
+ Hot memory now separates human authorship, operational state, and evidence:
198
+
199
+ - **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
200
+ - **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
201
+ - **`MEMORY_EVENTS.jsonl` is the append-only authority.** Producers publish to the outbox with exclusive creation, then the projector serializes append + projection under a lock. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
202
+ - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
203
+
204
+ Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
205
+
206
+ ### Injection and budgets
207
+
208
+ `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; CORE reserves up to 4 KiB, SHARED up to 6 KiB, and each line is capped at 320 characters. Under pressure, lessons are removed first, then non-current changes. CORE and SHARED are never prefix-sliced: a missing, invalid, or over-budget layer becomes a visible, repairable `<wk_memory_error>`.
209
+
210
+ `DIGEST.md` is no longer the operational handoff: it remains the `/brain-recall` bridge and legacy-vault fallback. A vault without SHARED receives CORE+DIGEST with a deprecation warning; migrate during the compatibility window:
211
+
212
+ ```bash
213
+ wendkeep memory status --gate --vault .MyApp-vault
214
+ wendkeep memory migrate --vault .MyApp-vault # preview, zero writes
215
+ wendkeep memory migrate --apply --vault .MyApp-vault # backup + candidates + v2 bundle
216
+ ```
217
+
218
+ ### Health and recovery
219
+
220
+ Use `wendkeep memory status --gate --vault <vault>` in CI and before `verify`/`archive`. The critical `memory-health` sensor blocks ledger/outbox/bundle corruption, revision/cursor/hash lag, and active conflicts. A valid pending outbox or ordinary candidate is recoverable and remains a warning rather than blocking.
221
+
222
+ If status blocks, preserve the evidence and run `wendkeep memory repair --vault <vault>` to save a backup of the corrupt ledger, retain valid lines, and re-project. Then run `status --gate` again. Conflicts require explicit curation with `memory promote <id>` or `memory reject <id>`; doctor only diagnoses.
223
+
190
224
  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.
191
225
 
192
226
  ## Retroactive memory (`import`) — install today, remember yesterday
@@ -302,11 +336,11 @@ is the real proof.
302
336
  ## How it works
303
337
 
304
338
  ```
305
- agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──▶ .brain index + Obsidian graph
306
- (Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
339
+ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──▶ .brain memory + Obsidian graph
340
+ (Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
307
341
  ```
308
342
 
309
- The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates the token/cost table, and (idempotently) emits any decision/bug/learning notes. On `SessionStart` — startup, `/clear` and `/compact` — `brain-inject` injects back curated memory (CORE + DIGEST), every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
343
+ The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
310
344
 
311
345
  The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
312
346
 
package/README.md CHANGED
@@ -38,7 +38,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
38
38
  |---|---|
39
39
  | **Capture** — every turn, on disk | `SessionStart` / `Stop` hooks write each session to a dated Markdown note: prompts, iterations, files touched, wikilinks. |
40
40
  | **Derive** — decisions, bugs, learnings | Pulled from the transcript into their own notes, backlinked to the session. Your history becomes navigable, not archival. |
41
- | **Recall** — injected back | A budget‑capped `CORE` + `DIGEST` and every open change are fed to the agent at the next `SessionStart`. It resumes where it left off. |
41
+ | **Recall** — injected back | Canonical `CORE` + operational `SHARED_MEMORY`, with explicit budgets, are injected on `startup`, `/clear`, and `/compact`; `DIGEST` remains a deep-recall index and legacy fallback. |
42
42
  | **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault. |
43
43
  | **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
44
44
  | **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
@@ -71,9 +71,9 @@ npx wendkeep init
71
71
 
72
72
  Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
73
73
  6. 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`. Re-apply it any time on an existing vault with `wendkeep theme sync` — Obsidian owns `graph.json` and can drop the color groups (a grey graph); the re-sync restores them without a full re-`init`.
74
- 7. 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).
74
+ 7. Seed **Shared Project Memory v2** without overwriting existing artifacts: `.brain/CORE.md` (hand-curated canonical truth), `.brain/SHARED_MEMORY.md` (generated operational state), `.brain/MEMORY_EVENTS.jsonl` (append-only ledger), `.brain/MEMORY_CANDIDATES.jsonl` (curation queue), and `.brain/COMPACTION_PROTOCOL.md`. The durable outbox appears under `.brain/memory-outbox/` when events exist; `DIGEST.md` and `index.jsonl` remain deep recall. Everything stays local to the vault.
75
75
  8. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify` (some ship templates — e.g. `wk-verify`'s `verdict-template.json` + reviewer prompt). `init` runs `wendkeep sync-defs` for you, delivering the skills to `.claude/skills/` and `.agents/skills/`, and the agent definitions (`.brain/agents/*.toml`) to `.codex/agents/`, plus a managed section in `AGENTS.md` that indexes the skills for Codex; `sync-defs --check` detects stale copies (re-run `sync-defs` after editing `.brain`).
76
- 9. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` — a critical `memory-validation` sensor (`npx wendkeep validate-memory`) plus one for each of `typecheck` / `test` / `lint` / `build` found in your `package.json`. Add your own with `wendkeep sensors add`. Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
76
+ 9. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` — critical memory validation/health sensors plus one for each of `typecheck` / `test` / `lint` / `build` found in your `package.json`. `memory-health` blocks delivery on corruption, projection divergence, or an active conflict; pending outbox events and ordinary candidates are warnings. Add sensors with `wendkeep sensors add`. Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
77
77
 
78
78
  ```bash
79
79
  npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (no companions unless you ask)
@@ -182,11 +182,45 @@ which the reseed never touches.
182
182
  | `wendkeep renumber-learnings` | Same for `06-Aprendizados`/`06-Learnings` → `APR-NNNN-<slug>`. |
183
183
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). `--change <slug>` ties the lesson to a change; `--vault P`. |
184
184
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift, `--reseed` refreshes the `wk-*` skills from the installed version's seeds. |
185
- | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
185
+ | `wendkeep memory status [--gate] --vault P` | Inspect the v2 bundle without mutating it. `--gate` exits 1 only for a blocking state; warnings keep exit code 0. |
186
+ | `wendkeep memory migrate [--apply] --vault P` | Convert a legacy `SHARED_MEMORY.md`. Dry-run by default; `--apply` creates a backup, turns legacy content into candidates, and publishes a valid v2 projection without editing CORE. |
187
+ | `wendkeep memory repair --vault P` | Repair a partial/corrupt ledger under lock, preserving the original bytes in a `.bak`, retaining valid events, and re-projecting state. |
188
+ | `wendkeep memory promote <candidate> --vault P` | Promote a candidate by ID by appending an auditable event; never edits the ledger in place. |
189
+ | `wendkeep memory reject <candidate> --vault P` | Reject a candidate by ID by appending the decision to the audit history. |
190
+ | `wendkeep validate-memory [path]` | Compatibility mode: validate `.brain/CORE.md` only (cap 25, 3 sections, no secrets/PII). Use `--vault <path>` to validate CORE + ledger + SHARED as a v2 bundle. |
186
191
  | `wendkeep theme sync [--vault P]` | Re-apply the colour system (CSS snippet + graph groups) to an existing vault — recovers a grey graph without re-running `init`. |
187
- | `wendkeep doctor [--vault P]` | Vault health check. Beyond session and registry integrity, it reports in sections: `[links]` (graph orphans), `[notas]` (stacked frontmatter), `[preços]` (a model missing from the price table, which would close the session at zero cost) and `[derivadas]` (stale body sections) each with its repair command alongside. |
192
+ | `wendkeep doctor [--vault P]` | Read-only vault health check. Beyond sessions/registry, links, notes, prices, and derived sections, it checks the v2 bundle and points to `memory status --gate` or `memory repair`; doctor never projects or repairs by itself. |
188
193
  | `wendkeep --version` / `--help` | Version / usage. |
189
194
 
195
+ ## Shared Project Memory v2
196
+
197
+ Hot memory now separates human authorship, operational state, and evidence:
198
+
199
+ - **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
200
+ - **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
201
+ - **`MEMORY_EVENTS.jsonl` is the append-only authority.** Producers publish to the outbox with exclusive creation, then the projector serializes append + projection under a lock. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
202
+ - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
203
+
204
+ Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
205
+
206
+ ### Injection and budgets
207
+
208
+ `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; CORE reserves up to 4 KiB, SHARED up to 6 KiB, and each line is capped at 320 characters. Under pressure, lessons are removed first, then non-current changes. CORE and SHARED are never prefix-sliced: a missing, invalid, or over-budget layer becomes a visible, repairable `<wk_memory_error>`.
209
+
210
+ `DIGEST.md` is no longer the operational handoff: it remains the `/brain-recall` bridge and legacy-vault fallback. A vault without SHARED receives CORE+DIGEST with a deprecation warning; migrate during the compatibility window:
211
+
212
+ ```bash
213
+ wendkeep memory status --gate --vault .MyApp-vault
214
+ wendkeep memory migrate --vault .MyApp-vault # preview, zero writes
215
+ wendkeep memory migrate --apply --vault .MyApp-vault # backup + candidates + v2 bundle
216
+ ```
217
+
218
+ ### Health and recovery
219
+
220
+ Use `wendkeep memory status --gate --vault <vault>` in CI and before `verify`/`archive`. The critical `memory-health` sensor blocks ledger/outbox/bundle corruption, revision/cursor/hash lag, and active conflicts. A valid pending outbox or ordinary candidate is recoverable and remains a warning rather than blocking.
221
+
222
+ If status blocks, preserve the evidence and run `wendkeep memory repair --vault <vault>` to save a backup of the corrupt ledger, retain valid lines, and re-project. Then run `status --gate` again. Conflicts require explicit curation with `memory promote <id>` or `memory reject <id>`; doctor only diagnoses.
223
+
190
224
  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.
191
225
 
192
226
  ## Retroactive memory (`import`) — install today, remember yesterday
@@ -302,11 +336,11 @@ is the real proof.
302
336
  ## How it works
303
337
 
304
338
  ```
305
- agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──▶ .brain index + Obsidian graph
306
- (Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
339
+ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──▶ .brain memory + Obsidian graph
340
+ (Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
307
341
  ```
308
342
 
309
- The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates the token/cost table, and (idempotently) emits any decision/bug/learning notes. On `SessionStart` — startup, `/clear` and `/compact` — `brain-inject` injects back curated memory (CORE + DIGEST), every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
343
+ The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
310
344
 
311
345
  The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
312
346
 
package/bin/wendkeep.mjs CHANGED
@@ -94,9 +94,11 @@ Usage:
94
94
  in session notes from the linked derived notes — the body used to lag
95
95
  behind the closing block. Dry-run by default · --apply · --json.
96
96
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
97
+ wendkeep memory <sub> Shared memory v2: status | migrate [--apply] | repair |
98
+ promote <candidate> | reject <candidate>. --vault P.
97
99
  wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
98
- protocol (cap 25, 3 sections, no secrets/PII). Uses
99
- --vault <path> or OBSIDIAN_VAULT_PATH if no path given.
100
+ protocol (cap 25, 3 sections, no secrets/PII).
101
+ --vault <path> validates the complete v2 bundle.
100
102
  wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
101
103
  project: .brain/agents/*.toml -> .codex/agents,
102
104
  .brain/skills/<name> -> .claude/skills + .agents/skills. --vault P --project P.
@@ -180,8 +182,18 @@ async function main() {
180
182
  break;
181
183
  }
182
184
  case 'validate-memory': {
183
- const { runValidateMemory } = await import('../src/validate-core.mjs');
184
- runValidateMemory(rest);
185
+ if (rest.includes('--vault') || rest.some((item) => item.startsWith('--vault='))) {
186
+ const { runValidateMemoryBundle } = await import('../src/memory.mjs');
187
+ runValidateMemoryBundle(rest);
188
+ } else {
189
+ const { runValidateMemory } = await import('../src/validate-core.mjs');
190
+ runValidateMemory(rest);
191
+ }
192
+ break;
193
+ }
194
+ case 'memory': {
195
+ const { runMemory } = await import('../src/memory.mjs');
196
+ runMemory(rest);
185
197
  break;
186
198
  }
187
199
  case 'sync-defs': {
@@ -1,8 +1,8 @@
1
1
  // .agent/hooks/brain-inject.mjs
2
2
  // Injeção da camada quente no SessionStart (Claude/Codex/Copilot): CORE curado +
3
- // DIGEST auto + 1-linha pointer do recall + backlog completo de changes. Nunca derruba o hook.
3
+ // SHARED operacional no v2; DIGEST fica no fallback legado/recall. Nunca derruba o hook.
4
4
  // Uso (hook): node .agent/hooks/brain-inject.mjs (input JSON via stdin)
5
- import { readFileSync } from 'node:fs';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
@@ -11,6 +11,8 @@ import { buildActiveChangeInjection, changeCtxState, writeSentinel } from './cha
11
11
  import { buildLessonsInjection } from './lessons-core.mjs';
12
12
  import { getLocale } from './locale.mjs';
13
13
  import { resolveSessionEntry } from './session-identity.mjs';
14
+ import { sanitizeMemoryText, validateSharedMemory } from './memory-schema.mjs';
15
+ import { validateCore } from '../src/validate-core.mjs';
14
16
 
15
17
  // The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
16
18
  // standing instruction the model plans in chat, leaves the change scaffold raw and forces the
@@ -41,30 +43,194 @@ function processRouter(localeId) {
41
43
  ].join('\n');
42
44
  }
43
45
 
44
- const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
46
+ const INJECTION_LIMITS = Object.freeze({
47
+ totalBytes: 24 * 1024,
48
+ lineChars: 320,
49
+ coreBytes: 4 * 1024,
50
+ sharedBytes: 6 * 1024,
51
+ attentionBytes: 1024,
52
+ recallBytes: 512,
53
+ });
54
+
55
+ function readMemoryFile(dir, name) {
56
+ try { return readFileSync(join(dir, name), 'utf8').replace(/\r\n/g, '\n').trim(); }
57
+ catch { return ''; }
58
+ }
59
+
60
+ function byteLength(value) {
61
+ return Buffer.byteLength(String(value || ''), 'utf8');
62
+ }
63
+
64
+ function xmlAttr(value) {
65
+ return String(value ?? '')
66
+ .replaceAll('&', '&amp;')
67
+ .replaceAll('"', '&quot;')
68
+ .replaceAll('<', '&lt;')
69
+ .replaceAll('>', '&gt;');
70
+ }
71
+
72
+ function safeError(layer, reasons) {
73
+ const detail = sanitizeMemoryText((reasons || []).join('; ') || 'arquivo ausente ou inválido')
74
+ .replace(/\s+/g, ' ');
75
+ const max = 260;
76
+ const visible = detail.length <= max ? detail : `${detail.slice(0, max - 20)} … [erro resumido]`;
77
+ return `<wk_memory_error layer="${layer}" repair="wendkeep memory status --gate">${visible}</wk_memory_error>`;
78
+ }
79
+
80
+ function validateCoreLayer(raw) {
81
+ if (!raw) return { ok: false, rendered: safeError('core', ['CORE.md ausente']) };
82
+ const sanitized = sanitizeMemoryText(raw);
83
+ const validation = validateCore(raw);
84
+ const errors = [...validation.errors];
85
+ if (sanitized !== raw) errors.push('CORE exige sanitização de segredo, PII, path local ou payload de harness');
86
+ if (byteLength(sanitized) > INJECTION_LIMITS.coreBytes) {
87
+ errors.push(`CORE excede ${INJECTION_LIMITS.coreBytes} bytes`);
88
+ }
89
+ sanitized.split('\n').forEach((line, index) => {
90
+ if (line.length > INJECTION_LIMITS.lineChars) errors.push(`CORE linha ${index + 1} excede ${INJECTION_LIMITS.lineChars} caracteres`);
91
+ });
92
+ return errors.length
93
+ ? { ok: false, rendered: safeError('core', errors) }
94
+ : { ok: true, rendered: sanitized };
95
+ }
96
+
97
+ function validateSharedLayer(raw) {
98
+ if (!raw) return { ok: false, rendered: safeError('shared', ['SHARED_MEMORY.md ausente']) };
99
+ // Boundary sanitization is deliberately repeated even though the projector already sanitizes.
100
+ const sanitized = sanitizeMemoryText(raw);
101
+ const validation = validateSharedMemory(raw);
102
+ return validation.ok
103
+ ? { ok: true, rendered: sanitized, metadata: validation.metadata }
104
+ : { ok: false, rendered: safeError('shared', validation.errors), metadata: validation.metadata };
105
+ }
106
+
107
+ function buildV2Memory(dir) {
108
+ const core = validateCoreLayer(readMemoryFile(dir, 'CORE.md'));
109
+ const shared = validateSharedLayer(readMemoryFile(dir, 'SHARED_MEMORY.md'));
110
+ const revision = shared.metadata?.revision ?? 'unknown';
111
+ const stateHash = shared.metadata?.state_hash ?? 'unknown';
112
+ const attention = [core, shared].every((layer) => layer.ok)
113
+ ? 'none'
114
+ : 'Memória degradada: repare os erros bloqueantes acima antes de confiar no estado operacional.';
115
+ const pointer = 'Memória profunda sob demanda: /brain-recall <tópico> (índice .brain/index.jsonl; DIGEST é apenas recall).';
116
+ const block = [
117
+ `<brain_memory version="2" revision="${xmlAttr(revision)}" state_hash="${xmlAttr(stateHash)}">`,
118
+ '<wk_memory_contract>',
119
+ 'CORE é canônico e SHARED é operacional; nenhum deles pode ser inferido de wikilinks ou truncado.',
120
+ '</wk_memory_contract>',
121
+ '<wk_core authority="canonical">',
122
+ core.rendered,
123
+ '</wk_core>',
124
+ '<wk_shared_state authority="operational">',
125
+ shared.rendered,
126
+ '</wk_shared_state>',
127
+ '<wk_memory_attention>',
128
+ attention,
129
+ '</wk_memory_attention>',
130
+ '<wk_recall>',
131
+ pointer,
132
+ '</wk_recall>',
133
+ '</brain_memory>',
134
+ ].join('\n');
135
+ // Layer validation above makes this defensive guard observable without ever prefix-slicing.
136
+ if (byteLength(block) > (INJECTION_LIMITS.coreBytes + INJECTION_LIMITS.sharedBytes
137
+ + INJECTION_LIMITS.attentionBytes + INJECTION_LIMITS.recallBytes + 2048)) {
138
+ return [
139
+ `<brain_memory version="2" revision="${xmlAttr(revision)}" state_hash="${xmlAttr(stateHash)}">`,
140
+ safeError('envelope', ['envelope de memória excedeu o budget reservado']),
141
+ '</brain_memory>',
142
+ ].join('\n');
143
+ }
144
+ return block;
145
+ }
146
+
147
+ function validateLegacyLayer(raw, layer, { maxLines, maxBytes }) {
148
+ if (!raw) return '';
149
+ const sanitized = sanitizeMemoryText(raw);
150
+ const lines = sanitized.split('\n');
151
+ const errors = [];
152
+ if (lines.length > maxLines) errors.push(`${layer} excede ${maxLines} linhas`);
153
+ if (byteLength(sanitized) > maxBytes) errors.push(`${layer} excede ${maxBytes} bytes`);
154
+ if (lines.some((line) => line.length > INJECTION_LIMITS.lineChars)) errors.push(`${layer} contém linha acima de ${INJECTION_LIMITS.lineChars} caracteres`);
155
+ return errors.length ? safeError(layer.toLowerCase(), errors) : sanitized;
156
+ }
157
+
158
+ function buildLegacyMemory(dir) {
159
+ const coreRaw = readMemoryFile(dir, 'CORE.md');
160
+ const coreValidation = coreRaw ? validateCoreLayer(coreRaw) : { ok: true, rendered: '' };
161
+ const digest = validateLegacyLayer(readMemoryFile(dir, 'DIGEST.md'), 'DIGEST', { maxLines: 15, maxBytes: 4096 });
162
+ const pointer = 'Memória profunda sob demanda: /brain-recall <tópico> (índice .brain/index.jsonl).';
163
+ return [
164
+ '<brain_memory>',
165
+ '<wk_memory_legacy_warning>Vault legado: CORE+DIGEST será removido após uma release; migre para SHARED_MEMORY v2.</wk_memory_legacy_warning>',
166
+ coreValidation.rendered,
167
+ digest,
168
+ pointer,
169
+ '</brain_memory>',
170
+ ].filter(Boolean).join('\n');
171
+ }
172
+
173
+ function joinInjection(parts) {
174
+ return sanitizeMemoryText(parts.filter(Boolean).join('\n'));
175
+ }
176
+
177
+ function boundAncillaryText(text, reservedChars = 0) {
178
+ if (text.length + reservedChars <= INJECTION_LIMITS.lineChars) return text;
179
+ const marker = ' … [linha resumida pelo budget]';
180
+ return `${text.slice(0, INJECTION_LIMITS.lineChars - reservedChars - marker.length)}${marker}`;
181
+ }
182
+
183
+ function budgetNotice(priority, layer, message) {
184
+ return `<wk_budget_notice priority="${priority}" layer="${layer}">${message}</wk_budget_notice>`;
185
+ }
45
186
 
46
187
  export function buildInjection(vaultBase, input = {}) {
47
188
  const dir = brainDir(vaultBase);
48
- const read = (name) => {
49
- try { return readFileSync(join(dir, name), 'utf8').trim(); } catch { return ''; }
50
- };
51
- const pointer = 'Memória profunda sob demanda: /brain-recall <tópico> (índice .brain/index.jsonl).';
52
- // Quando CORE e DIGEST não existem, ''.split('\n') vira [''] — o filter derruba essa
53
- // linha vazia para o caso "só pointer" ficar com exatamente 3 linhas.
54
- let lines = [read('CORE.md'), read('DIGEST.md')].filter(Boolean).join('\n\n').split('\n').filter((l, i, a) => a.length > 1 || l);
55
- if (lines.length > MAX_LINES) {
56
- lines = lines.slice(0, MAX_LINES);
57
- lines.push('*…truncado pelo budget — fonte completa: .brain/CORE.md + .brain/DIGEST.md*');
58
- }
59
- const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
189
+ const brain = existsSync(join(dir, 'SHARED_MEMORY.md')) ? buildV2Memory(dir) : buildLegacyMemory(dir);
60
190
  const router = processRouter(getLocale(vaultBase).id);
61
- const change = buildActiveChangeInjection(vaultBase);
62
- const lessons = buildLessonsInjection(vaultBase);
63
191
  const { identity, entry } = resolveSessionEntry(vaultBase, input);
64
192
  const focus = identity.state === 'resolved' && entry?.change_slug
65
- ? `<session_change>Change vinculada a esta sessão: ${entry.change_slug}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.</session_change>`
193
+ ? `<session_change>${boundAncillaryText(`Change vinculada a esta sessão: ${entry.change_slug}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.`, '<session_change></session_change>'.length)}</session_change>`
66
194
  : '';
67
- return [brain, router, focus, change, lessons].filter(Boolean).join('\n');
195
+ const allChanges = buildActiveChangeInjection(vaultBase, { maxLineChars: INJECTION_LIMITS.lineChars });
196
+ const lessons = buildLessonsInjection(vaultBase, { maxLineChars: INJECTION_LIMITS.lineChars });
197
+
198
+ // Global priority is deterministic: memory/router/focus, then changes, then lessons.
199
+ let output = joinInjection([brain, router, focus, allChanges, lessons]);
200
+ if (byteLength(output) <= INJECTION_LIMITS.totalBytes) return output;
201
+
202
+ // First pressure step: lessons are fully removable and remain available in the vault.
203
+ const lessonsEvicted = budgetNotice(1, 'lessons', 'Lessons omitidas primeiro pelo budget global.');
204
+ output = joinInjection([brain, router, focus, allChanges, lessonsEvicted]);
205
+ if (byteLength(output) <= INJECTION_LIMITS.totalBytes) return output;
206
+
207
+ // Second pressure step: non-current changes leave the hot context before the current one.
208
+ const nonCurrentEvicted = budgetNotice(2, 'non-current-changes', 'Changes não atuais omitidas depois das lessons.');
209
+ const currentChange = buildActiveChangeInjection(vaultBase, {
210
+ currentOnly: true,
211
+ maxLineChars: INJECTION_LIMITS.lineChars,
212
+ });
213
+ output = joinInjection([brain, router, focus, currentChange, lessonsEvicted, nonCurrentEvicted]);
214
+ if (byteLength(output) <= INJECTION_LIMITS.totalBytes) return output;
215
+
216
+ // Last step caps only the current change block, with an explicit marker and closed wrapper.
217
+ const currentSummarized = budgetNotice(3, 'current-change', 'Change atual resumida por último; blocker e início da fila foram preservados.');
218
+ const fixed = joinInjection([brain, router, focus, lessonsEvicted, nonCurrentEvicted, currentSummarized]);
219
+ const remaining = Math.max(512, INJECTION_LIMITS.totalBytes - byteLength(fixed) - 1);
220
+ const boundedCurrent = buildActiveChangeInjection(vaultBase, {
221
+ currentOnly: true,
222
+ maxBytes: remaining,
223
+ maxLineChars: INJECTION_LIMITS.lineChars,
224
+ });
225
+ return joinInjection([
226
+ brain,
227
+ router,
228
+ focus,
229
+ lessonsEvicted,
230
+ nonCurrentEvicted,
231
+ boundedCurrent,
232
+ currentSummarized,
233
+ ]);
68
234
  }
69
235
 
70
236
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -245,30 +245,67 @@ export function allChangesState(vaultBase) {
245
245
  return { current, changes, pointerWarning, hash: tasksHashOf(fingerprint.join('\n')) };
246
246
  }
247
247
 
248
- export function renderOpenChanges(state, { tag = 'open_changes' } = {}) {
248
+ function boundInjectionLine(line, maxLineChars) {
249
+ if (!Number.isFinite(maxLineChars) || line.length <= maxLineChars) return line;
250
+ const marker = ' … [linha resumida pelo budget]';
251
+ return `${line.slice(0, Math.max(0, maxLineChars - marker.length))}${marker}`;
252
+ }
253
+
254
+ function capInjectionBlock(lines, { tag, maxBytes, maxLineChars }) {
255
+ const bounded = lines.map((line) => boundInjectionLine(line, maxLineChars));
256
+ if (!Number.isFinite(maxBytes) || Buffer.byteLength(bounded.join('\n'), 'utf8') <= maxBytes) {
257
+ return bounded.join('\n');
258
+ }
259
+ const closing = tag ? `</${tag}>` : '';
260
+ const marker = '- … conteúdo restante omitido pelo budget de injeção.';
261
+ const body = tag ? bounded.slice(0, -1) : bounded;
262
+ const out = [];
263
+ for (const line of body) {
264
+ const candidate = [...out, line, marker, closing].filter(Boolean).join('\n');
265
+ if (Buffer.byteLength(candidate, 'utf8') > maxBytes) break;
266
+ out.push(line);
267
+ }
268
+ // The tag itself and an explicit marker are the minimum valid degraded block.
269
+ if (!out.length && tag) out.push(`<${tag}>`);
270
+ out.push(marker);
271
+ if (closing) out.push(closing);
272
+ return out.join('\n');
273
+ }
274
+
275
+ export function renderOpenChanges(state, {
276
+ tag = 'open_changes',
277
+ currentOnly = false,
278
+ maxBytes = Number.POSITIVE_INFINITY,
279
+ maxLineChars = 320,
280
+ } = {}) {
249
281
  if (!state?.changes?.length && !state?.pointerWarning) return '';
250
282
  const lines = [];
251
283
  if (tag) lines.push(`<${tag}>`);
252
284
  if (state.current) lines.push(`Change atual (comandos sem --change): ${state.current}.`);
253
285
  else lines.push('Nenhuma change atual selecionada; comandos sem --change continuam recusados.');
254
286
  if (state.pointerWarning) lines.push(`Aviso: ${state.pointerWarning}.`);
255
- for (const change of state.changes || []) {
287
+ const visibleChanges = currentOnly
288
+ ? (state.changes || []).filter((change) => change.current)
289
+ : (state.changes || []);
290
+ for (const change of visibleChanges) {
256
291
  const label = change.current ? 'ATUAL' : 'ABERTA';
257
292
  lines.push(`### ${label} — ${change.slug} (${change.openCount} aberta(s), ${change.doneCount} concluída(s))`);
258
293
  if (change.warning) lines.push(`- Aviso: ${change.warning}.`);
259
294
  else if (!change.openTasks.length) lines.push('- Nenhuma tarefa aberta.');
260
295
  else for (const task of change.openTasks) lines.push(`- [ ] ${task.id} ${task.text}`);
261
296
  }
297
+ const omittedChanges = (state.changes || []).length - visibleChanges.length;
298
+ if (omittedChanges > 0) lines.push(`- ${omittedChanges} change(s) não atual(is) omitida(s) pelo budget global.`);
262
299
  if (state.current) lines.push('Para change atual: `wendkeep change done <id>`; antes de archive: `wendkeep verify`.');
263
300
  lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change use <slug>` ou use `--change <slug>` quando disponível.');
264
301
  if (tag) lines.push(`</${tag}>`);
265
- return lines.join('\n');
302
+ return capInjectionBlock(lines, { tag, maxBytes, maxLineChars });
266
303
  }
267
304
 
268
305
  // Mantém o nome exportado para consumidores internos existentes, mas agora injeta o backlog
269
306
  // completo em vez de ocultar changes não selecionadas.
270
- export function buildActiveChangeInjection(vaultBase) {
271
- return renderOpenChanges(allChangesState(vaultBase));
307
+ export function buildActiveChangeInjection(vaultBase, options = {}) {
308
+ return renderOpenChanges(allChangesState(vaultBase), options);
272
309
  }
273
310
 
274
311
  export function activeChangeLink(vaultBase) {
@@ -36,7 +36,7 @@ export function addLesson(vaultBase, { trigger, lesson, sourceChange = '', dateS
36
36
 
37
37
  // Compact <lessons> block of the most recent lessons (filename-sorted desc; a date prefix
38
38
  // makes that chronological). Budget-capped by `max`. '' when there are none.
39
- export function buildLessonsInjection(vaultBase, { max = 5 } = {}) {
39
+ export function buildLessonsInjection(vaultBase, { max = 5, maxLineChars = 320 } = {}) {
40
40
  const dir = join(vaultBase, '.brain', 'lessons');
41
41
  let files;
42
42
  try { files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort().reverse().slice(0, max); }
@@ -45,7 +45,13 @@ export function buildLessonsInjection(vaultBase, { max = 5 } = {}) {
45
45
  for (const f of files) {
46
46
  try {
47
47
  const body = readFileSync(join(dir, f), 'utf8').replace(/^---[\s\S]*?---\n/, '').trim().split('\n')[0];
48
- if (body) lines.push(`- ${body}`);
48
+ if (body) {
49
+ const line = `- ${body}`;
50
+ const marker = ' … [linha resumida pelo budget]';
51
+ lines.push(line.length <= maxLineChars
52
+ ? line
53
+ : `${line.slice(0, Math.max(0, maxLineChars - marker.length))}${marker}`);
54
+ }
49
55
  } catch { /* skip */ }
50
56
  }
51
57
  return lines.length ? `<lessons>\nLições do projeto (de falhas anteriores):\n${lines.join('\n')}\n</lessons>` : '';