wendkeep 0.58.0 → 0.58.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,20 @@ 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.1] — 2026-07-26
8
+
9
+ ### Fixed
10
+
11
+ - **Vaults com `SHARED_MEMORY.md` legado voltam a atualizar sem bloquear o doctor.** `init` e
12
+ `sync` ainda preservam todos os bytes existentes e podem criar os sidecars v2 ausentes,
13
+ mas sidecars vazios não ativam a memória v2. `memory status --gate` reporta o estado
14
+ `legacy` como aviso não bloqueante e `brain-inject` mantém CORE+DIGEST durante a janela de
15
+ compatibilidade.
16
+ - **`SessionStop` não migra memória implicitamente.** Enquanto o vault permanecer legado, o
17
+ Stop não grava outbox, ledger, candidates nem reescreve SHARED. A transição acontece somente
18
+ com `wendkeep memory migrate --apply`; conteúdo com assinatura/evidência v2 corrompida continua
19
+ visível e bloqueante, sem fallback silencioso.
20
+
7
21
  ## [0.58.0] — 2026-07-26
8
22
 
9
23
  ### Added
@@ -12,6 +12,7 @@ import { buildLessonsInjection } from './lessons-core.mjs';
12
12
  import { getLocale } from './locale.mjs';
13
13
  import { resolveSessionEntry } from './session-identity.mjs';
14
14
  import { sanitizeMemoryText, validateSharedMemory } from './memory-schema.mjs';
15
+ import { detectMemoryMode } from './memory-mode.mjs';
15
16
  import { validateCore } from '../src/validate-core.mjs';
16
17
 
17
18
  // The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
@@ -186,7 +187,7 @@ function budgetNotice(priority, layer, message) {
186
187
 
187
188
  export function buildInjection(vaultBase, input = {}) {
188
189
  const dir = brainDir(vaultBase);
189
- const brain = existsSync(join(dir, 'SHARED_MEMORY.md')) ? buildV2Memory(dir) : buildLegacyMemory(dir);
190
+ const brain = detectMemoryMode(vaultBase).mode === 'v2' ? buildV2Memory(dir) : buildLegacyMemory(dir);
190
191
  const router = processRouter(getLocale(vaultBase).id);
191
192
  const { identity, entry } = resolveSessionEntry(vaultBase, input);
192
193
  const focus = identity.state === 'resolved' && entry?.change_slug
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { classifySharedMemory } from './memory-schema.mjs';
5
+
6
+ export const LEGACY_MEMORY_WARNING = 'Vault legado: CORE+DIGEST permanece ativo; execute `wendkeep memory migrate --apply` quando a curadoria estiver pronta.';
7
+
8
+ function readText(path) {
9
+ if (!existsSync(path)) return { exists: false, content: '', error: null };
10
+ try { return { exists: true, content: readFileSync(path, 'utf8'), error: null }; }
11
+ catch (error) { return { exists: true, content: '', error }; }
12
+ }
13
+
14
+ function hasOutboxEvidence(path) {
15
+ if (!existsSync(path)) return false;
16
+ try { return readdirSync(path, { withFileTypes: true }).some((entry) => entry.isFile()); }
17
+ catch { return true; }
18
+ }
19
+
20
+ /** A single, read-only mode decision shared by injection, health and SessionStop. */
21
+ export function detectMemoryMode(vaultBase) {
22
+ const brain = join(vaultBase, '.brain');
23
+ const shared = readText(join(brain, 'SHARED_MEMORY.md'));
24
+ if (shared.error) return { mode: 'v2', reason: 'shared-unreadable' };
25
+ const classified = classifySharedMemory(shared.content);
26
+ if (classified.mode === 'v2') return classified;
27
+
28
+ const ledger = readText(join(brain, 'MEMORY_EVENTS.jsonl'));
29
+ if (ledger.error) return { mode: 'v2', reason: 'ledger-unreadable' };
30
+ const candidates = readText(join(brain, 'MEMORY_CANDIDATES.jsonl'));
31
+ if (candidates.error) return { mode: 'v2', reason: 'candidates-unreadable' };
32
+ const ledgerHasEvents = ledger.content.trim().length > 0;
33
+ const candidatesHaveEntries = candidates.content.trim().length > 0;
34
+ const outboxHasEvents = hasOutboxEvidence(join(brain, 'memory-outbox'));
35
+ if (ledgerHasEvents || candidatesHaveEntries || outboxHasEvents) {
36
+ return { mode: 'v2', reason: 'operational-evidence' };
37
+ }
38
+ return classified;
39
+ }
@@ -293,3 +293,18 @@ export function validateSharedMemory(content, { eventIds } = {}) {
293
293
  sections: parsed.sections,
294
294
  };
295
295
  }
296
+
297
+ /**
298
+ * Classify SHARED from its own bytes without accepting malformed v2 as legacy.
299
+ * Empty v2 sidecar files are intentionally not part of this pure classification;
300
+ * vault-level operational evidence is handled by memory-mode.mjs.
301
+ */
302
+ export function classifySharedMemory(content) {
303
+ const text = String(content ?? '').replace(/\r\n/g, '\n');
304
+ if (validateSharedMemory(text).ok) return { mode: 'v2', reason: 'valid-v2' };
305
+ const hasV2Signature = /^(?:schema_version|event_cursor|state_hash)\s*:/m.test(text)
306
+ || /^# SHARED_MEMORY\s+[—-]\s+proje[cç][aã]o operacional gerada\s*$/mi.test(text);
307
+ return hasV2Signature
308
+ ? { mode: 'v2', reason: 'v2-signature' }
309
+ : { mode: 'legacy', reason: text.trim() ? 'legacy-shared' : 'shared-absent' };
310
+ }
@@ -14,6 +14,7 @@ import { mutateSessionNote } from './session-note-io.mjs';
14
14
  import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
15
15
  import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
16
16
  import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
17
+ import { detectMemoryMode } from './memory-mode.mjs';
17
18
  import { sanitizeMemoryText } from './memory-schema.mjs';
18
19
  import {
19
20
  ensureDir,
@@ -799,6 +800,9 @@ function shouldFinalizeSession() {
799
800
  }
800
801
 
801
802
  export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
803
+ if (detectMemoryMode(vaultBase).mode === 'legacy') {
804
+ return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
805
+ }
802
806
  const events = buildSessionMemoryEvents(handoff);
803
807
  const eventIds = events.map((event) => event.event_id);
804
808
  try {
@@ -12,6 +12,7 @@ import {
12
12
  } from './obsidian-common.mjs';
13
13
  import { getLocale } from './locale.mjs';
14
14
  import { parseSharedMemory, validateMemoryEvent } from './memory-schema.mjs';
15
+ import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
15
16
  import { reduceMemoryEvents } from './memory-store.mjs';
16
17
  import { validateMemoryBundle } from '../src/validate-memory.mjs';
17
18
 
@@ -101,7 +102,9 @@ const MEMORY_REPAIR_COMMAND = 'wendkeep memory repair --vault <vault>';
101
102
 
102
103
  function readJsonLines(path, label) {
103
104
  if (!existsSync(path)) return { items: [], errors: [] };
104
- const raw = readFileSync(path, 'utf8').replace(/\r\n/g, '\n');
105
+ let raw;
106
+ try { raw = readFileSync(path, 'utf8').replace(/\r\n/g, '\n'); }
107
+ catch (error) { return { items: [], errors: [`${label} ilegível: ${error?.message || error}`] }; }
105
108
  const lines = raw.endsWith('\n') ? raw.split('\n').slice(0, -1) : raw.split('\n');
106
109
  const items = [];
107
110
  const errors = [];
@@ -140,6 +143,25 @@ function inspectOutbox(vaultBase, projectId) {
140
143
  */
141
144
  export function checkMemoryBundle(vaultBase) {
142
145
  const brain = join(vaultBase, '.brain');
146
+ const mode = detectMemoryMode(vaultBase);
147
+ if (mode.mode === 'legacy') {
148
+ return {
149
+ ok: true,
150
+ status: 'legacy',
151
+ failures: [],
152
+ warnings: [LEGACY_MEMORY_WARNING],
153
+ metrics: {
154
+ schemaVersion: null,
155
+ revision: null,
156
+ eventCursor: null,
157
+ stateHash: null,
158
+ ledgerEvents: 0,
159
+ pendingOutbox: 0,
160
+ candidates: 0,
161
+ activeConflicts: 0,
162
+ },
163
+ };
164
+ }
143
165
  const bundle = validateMemoryBundle(vaultBase);
144
166
  const failures = [];
145
167
  const warnings = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.58.0",
3
+ "version": "0.58.1",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/memory.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import {
3
- copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync,
3
+ copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync,
4
4
  } from 'node:fs';
5
- import { join } from 'node:path';
5
+ import { dirname, join } from 'node:path';
6
6
  import { sanitizeMemoryText, renderSharedMemory, validateSharedMemory } from '../hooks/memory-schema.mjs';
7
7
  import {
8
8
  enqueueMemoryEvent, projectMemoryOutbox, repairMemoryLedger,
@@ -75,7 +75,11 @@ export function memoryStatus(vault) {
75
75
  return checkMemoryBundle(vault);
76
76
  }
77
77
 
78
- export function migrateMemory(vault, { apply = false, validateBundle = validateMemoryBundle } = {}) {
78
+ export function migrateMemory(vault, {
79
+ apply = false,
80
+ validateBundle = validateMemoryBundle,
81
+ publishArtifact = writeFileAtomic,
82
+ } = {}) {
79
83
  projectId(vault);
80
84
  const sharedPath = brainPath(vault, SHARED);
81
85
  const hadShared = existsSync(sharedPath);
@@ -95,16 +99,37 @@ export function migrateMemory(vault, { apply = false, validateBundle = validateM
95
99
  const sharedValidation = validateSharedMemory(emptyShared, { eventIds: new Set() });
96
100
  if (!sharedValidation.ok) throw new Error(`Migração inválida: ${sharedValidation.errors.join(' ')}`);
97
101
  if (backupPath && !existsSync(backupPath)) copyFileSync(sharedPath, backupPath);
102
+
103
+ // Build and validate a complete candidate vault away from the live paths. This makes
104
+ // the validation callback incapable of observing a half-published live bundle.
105
+ const stagingVault = mkdtempSync(join(dirname(vault), '.wendkeep-memory-stage-'));
106
+ const stagingBrain = join(stagingVault, BRAIN);
107
+ let stagedValidation;
108
+ try {
109
+ mkdirSync(stagingBrain, { recursive: true });
110
+ copyFileSync(brainPath(vault, 'CORE.md'), join(stagingBrain, 'CORE.md'));
111
+ copyFileSync(brainPath(vault, 'PROJECT.json'), join(stagingBrain, 'PROJECT.json'));
112
+ writeFileSync(join(stagingBrain, LEDGER), '', 'utf8');
113
+ writeFileSync(join(stagingBrain, SHARED), emptyShared, 'utf8');
114
+ writeFileSync(join(stagingBrain, CANDIDATES), candidateText(candidates), 'utf8');
115
+ stagedValidation = validateBundle(stagingVault);
116
+ if (!stagedValidation.ok) {
117
+ throw new Error(`Bundle migrado inválido: ${(stagedValidation.errors || []).join(' ')}`);
118
+ }
119
+ } finally {
120
+ rmSync(stagingVault, { recursive: true, force: true });
121
+ }
122
+
98
123
  const targets = [LEDGER, SHARED, CANDIDATES].map((name) => brainPath(vault, name));
99
124
  const before = new Map(targets.map((path) => [path, {
100
125
  existed: existsSync(path),
101
126
  content: existsSync(path) ? readFileSync(path, 'utf8') : '',
102
127
  }]));
103
128
  try {
104
- if (!existsSync(brainPath(vault, LEDGER))) writeFileAtomic(brainPath(vault, LEDGER), '');
105
- writeFileAtomic(sharedPath, emptyShared);
106
- writeFileAtomic(brainPath(vault, CANDIDATES), candidateText(candidates));
107
- const validation = validateBundle(vault);
129
+ if (!existsSync(brainPath(vault, LEDGER))) publishArtifact(brainPath(vault, LEDGER), '');
130
+ publishArtifact(sharedPath, emptyShared);
131
+ publishArtifact(brainPath(vault, CANDIDATES), candidateText(candidates));
132
+ const validation = validateMemoryBundle(vault);
108
133
  if (!validation.ok) throw new Error(`Bundle migrado inválido: ${validation.errors.join(' ')}`);
109
134
  return { status: 'migrated', alreadyV2: false, candidates: candidates.length, backupPath, validation };
110
135
  } catch (error) {