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 +14 -0
- package/hooks/brain-inject.mjs +2 -1
- package/hooks/memory-mode.mjs +39 -0
- package/hooks/memory-schema.mjs +15 -0
- package/hooks/session-stop.mjs +4 -0
- package/hooks/vault-health.mjs +23 -1
- package/package.json +1 -1
- package/src/memory.mjs +32 -7
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
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/hooks/memory-schema.mjs
CHANGED
|
@@ -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
|
+
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -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 {
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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, {
|
|
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)))
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const validation =
|
|
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) {
|