wendkeep 0.58.3 → 0.60.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.en.md +45 -3
  3. package/README.md +45 -3
  4. package/bin/wendkeep.mjs +54 -6
  5. package/docs/en/commands/changes-and-verification.md +9 -3
  6. package/docs/en/commands/getting-started.md +7 -3
  7. package/docs/en/commands/memory.md +20 -2
  8. package/docs/en/commands/operating-profiles.md +173 -0
  9. package/docs/en/commands/sessions-and-import.md +8 -4
  10. package/docs/en/commands/verify.md +12 -6
  11. package/docs/pt-BR/commands/changes-and-verification.md +9 -4
  12. package/docs/pt-BR/commands/getting-started.md +7 -3
  13. package/docs/pt-BR/commands/memory.md +18 -2
  14. package/docs/pt-BR/commands/operating-profiles.md +171 -0
  15. package/docs/pt-BR/commands/sessions-and-import.md +7 -3
  16. package/docs/pt-BR/commands/verify.md +11 -5
  17. package/hooks/brain-core.mjs +159 -159
  18. package/hooks/brain-inject.mjs +83 -26
  19. package/hooks/brain-recall.mjs +32 -32
  20. package/hooks/brain-reindex.mjs +13 -13
  21. package/hooks/change-context.mjs +24 -10
  22. package/hooks/change-core.mjs +174 -37
  23. package/hooks/change-guard.mjs +115 -16
  24. package/hooks/change-nag.mjs +20 -5
  25. package/hooks/change-warn.mjs +27 -9
  26. package/hooks/decision-capture.mjs +1 -1
  27. package/hooks/derived-sections.mjs +1 -1
  28. package/hooks/flow-core.mjs +891 -0
  29. package/hooks/flow-protected-policy.mjs +218 -0
  30. package/hooks/frontmatter-repair.mjs +3 -1
  31. package/hooks/git-snapshot.mjs +722 -0
  32. package/hooks/import-sessions.mjs +10 -5
  33. package/hooks/memory-mode.mjs +63 -13
  34. package/hooks/memory-store.mjs +309 -69
  35. package/hooks/obsidian-common.mjs +39 -55
  36. package/hooks/operating-profile-runtime.mjs +157 -0
  37. package/hooks/plan-capture.mjs +14 -3
  38. package/hooks/sensors-core.mjs +15 -3
  39. package/hooks/session-backfill.mjs +7 -2
  40. package/hooks/session-ensure.mjs +6 -4
  41. package/hooks/session-iteration.mjs +65 -0
  42. package/hooks/session-memory-lifecycle.mjs +10 -5
  43. package/hooks/session-note-io.mjs +130 -15
  44. package/hooks/session-observability.mjs +4 -2
  45. package/hooks/session-stop.mjs +65 -19
  46. package/hooks/spec-core.mjs +91 -12
  47. package/hooks/subagent-stop.mjs +4 -1
  48. package/hooks/subagent-usage.mjs +2 -2
  49. package/hooks/task-log.mjs +3 -1
  50. package/hooks/token-usage.mjs +1 -1
  51. package/hooks/vault-health.mjs +183 -37
  52. package/hooks/vault-path-safety.mjs +2 -0
  53. package/hooks/vault-runtime-store.mjs +558 -0
  54. package/package.json +10 -3
  55. package/packages/cli/package.json +5 -0
  56. package/packages/harness/package.json +5 -0
  57. package/packages/integrations/package.json +5 -0
  58. package/packages/mcp/package.json +5 -0
  59. package/packages/pi/package.json +5 -0
  60. package/packages/vault/package.json +6 -0
  61. package/packages/vault/src/index.mjs +2 -0
  62. package/packages/vault/src/project-vault.mjs +327 -0
  63. package/packages/vault/src/vault-path-safety.mjs +558 -0
  64. package/src/change.mjs +2 -1
  65. package/src/flow.mjs +232 -0
  66. package/src/init.mjs +26 -3
  67. package/src/memory.mjs +785 -35
  68. package/src/operating-profile.mjs +133 -0
  69. package/src/profile.mjs +224 -0
  70. package/src/project-vault.mjs +2 -221
  71. package/src/rebuild-costs.mjs +11 -4
  72. package/src/skills-seed.mjs +38 -16
  73. package/src/sync-defs.mjs +16 -7
  74. package/src/sync.mjs +9 -1
  75. package/src/taxonomy.mjs +8 -0
  76. package/src/validate-memory.mjs +21 -8
  77. package/src/verify.mjs +12 -2
@@ -13,7 +13,8 @@ import {
13
13
  import { getLocale } from './locale.mjs';
14
14
  import { parseSharedMemory, validateMemoryEvent } from './memory-schema.mjs';
15
15
  import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
16
- import { reduceMemoryEvents } from './memory-store.mjs';
16
+ import { deriveMemoryProjection } from './memory-store.mjs';
17
+ import { assertVaultPathSafe, assertVaultPathsSafe } from './vault-path-safety.mjs';
17
18
  import { validateMemoryBundle } from '../src/validate-memory.mjs';
18
19
 
19
20
  const DEFAULT_PENDING_PATTERNS = [
@@ -100,10 +101,21 @@ function linkedNotesFromSession(content) {
100
101
  const MEMORY_STATUS_COMMAND = 'wendkeep memory status --gate --vault <vault>';
101
102
  const MEMORY_REPAIR_COMMAND = 'wendkeep memory repair --vault <vault>';
102
103
 
103
- function readJsonLines(path, label) {
104
- if (!existsSync(path)) return { items: [], errors: [] };
104
+ function readJsonLines(vaultBase, path, label) {
105
+ let checked;
106
+ try {
107
+ checked = assertVaultPathSafe(vaultBase, path, { expectedType: 'file', label });
108
+ } catch (error) {
109
+ return { items: [], errors: [`${label} inseguro: ${error?.message || error}`] };
110
+ }
111
+ if (!checked.exists) return { items: [], errors: [] };
105
112
  let raw;
106
- try { raw = readFileSync(path, 'utf8').replace(/\r\n/g, '\n'); }
113
+ try {
114
+ checked = assertVaultPathSafe(vaultBase, checked.target, {
115
+ allowMissing: false, expectedType: 'file', label,
116
+ });
117
+ raw = readFileSync(checked.target, 'utf8').replace(/\r\n/g, '\n');
118
+ }
107
119
  catch (error) { return { items: [], errors: [`${label} ilegível: ${error?.message || error}`] }; }
108
120
  const lines = raw.endsWith('\n') ? raw.split('\n').slice(0, -1) : raw.split('\n');
109
121
  const items = [];
@@ -121,25 +133,107 @@ function readJsonLines(path, label) {
121
133
 
122
134
  function inspectOutbox(vaultBase, projectId) {
123
135
  const dir = join(vaultBase, '.brain', 'memory-outbox');
124
- if (!existsSync(dir)) return { count: 0, errors: [], eventIds: new Set() };
125
- const files = readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
136
+ let checked;
137
+ try {
138
+ checked = assertVaultPathSafe(vaultBase, dir, {
139
+ expectedType: 'directory', label: 'outbox de memória',
140
+ });
141
+ } catch (error) {
142
+ return {
143
+ count: 0,
144
+ errors: [`outbox insegura: ${error?.message || error}`],
145
+ eventIds: new Set(),
146
+ eventsById: new Map(),
147
+ };
148
+ }
149
+ if (!checked.exists) return {
150
+ count: 0, errors: [], eventIds: new Set(), eventsById: new Map(),
151
+ };
152
+ try {
153
+ checked = assertVaultPathSafe(vaultBase, checked.target, {
154
+ allowMissing: false, expectedType: 'directory', label: 'outbox de memória',
155
+ });
156
+ } catch (error) {
157
+ return {
158
+ count: 0,
159
+ errors: [`outbox insegura: ${error?.message || error}`],
160
+ eventIds: new Set(),
161
+ eventsById: new Map(),
162
+ };
163
+ }
164
+ const files = readdirSync(checked.target).filter((name) => name.endsWith('.json')).sort();
126
165
  const errors = [];
127
166
  const eventIds = new Set();
167
+ const eventsById = new Map();
128
168
  for (const name of files) {
129
- const path = join(dir, name);
169
+ const path = join(checked.target, name);
130
170
  try {
131
- const event = JSON.parse(readFileSync(path, 'utf8'));
171
+ const file = assertVaultPathSafe(vaultBase, path, {
172
+ allowMissing: false, expectedType: 'file', label: `evento ${name} da outbox`,
173
+ });
174
+ const event = JSON.parse(readFileSync(file.target, 'utf8'));
132
175
  const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
133
176
  if (!validation.ok) errors.push(`${name}: ${validation.errors.join(' ')}`);
134
- else eventIds.add(event.event_id);
177
+ else {
178
+ eventIds.add(event.event_id);
179
+ eventsById.set(event.event_id, event);
180
+ }
135
181
  } catch (error) {
136
182
  errors.push(`${name}: JSON inválido: ${error.message}`);
137
183
  }
138
184
  }
139
- return { count: files.length, errors, eventIds };
185
+ return {
186
+ count: files.length, errors, eventIds, eventsById,
187
+ };
188
+ }
189
+
190
+ function memoryMetrics() {
191
+ return {
192
+ schemaVersion: null,
193
+ revision: null,
194
+ eventCursor: null,
195
+ stateHash: null,
196
+ ledgerEvents: 0,
197
+ pendingOutbox: 0,
198
+ candidates: 0,
199
+ activeConflicts: 0,
200
+ };
140
201
  }
141
202
 
142
- function checkpointMatchesLedgerPrefix(checkpoint, eventIds, ledgerEvents) {
203
+ function blockedMemoryBoundary(error) {
204
+ return {
205
+ ok: false,
206
+ status: 'blocked',
207
+ failures: [`Boundary física da memória insegura: ${error?.message || error}`],
208
+ warnings: [],
209
+ metrics: memoryMetrics(),
210
+ };
211
+ }
212
+
213
+ function preflightMemoryBundle(vaultBase) {
214
+ const brain = join(vaultBase, '.brain');
215
+ assertVaultPathSafe(vaultBase, brain, {
216
+ expectedType: 'directory', label: 'raiz .brain da memória',
217
+ });
218
+ assertVaultPathsSafe(vaultBase, [
219
+ 'PROJECT.json', 'CORE.md', 'MEMORY_EVENTS.jsonl', 'SHARED_MEMORY.md',
220
+ 'MEMORY_CANDIDATES.jsonl',
221
+ ].map((name) => ({
222
+ path: join(brain, name), expectedType: 'file', label: `${name} ilegível ou inseguro`,
223
+ })));
224
+ const outbox = assertVaultPathSafe(vaultBase, join(brain, 'memory-outbox'), {
225
+ expectedType: 'directory', label: 'outbox de memória',
226
+ });
227
+ if (!outbox.exists) return;
228
+ const entries = readdirSync(outbox.target);
229
+ for (const name of entries) {
230
+ assertVaultPathSafe(vaultBase, join(outbox.target, name), {
231
+ allowMissing: false, label: `entrada ${name} da outbox de memória`,
232
+ });
233
+ }
234
+ }
235
+
236
+ function checkpointMatchesLedgerPrefix(checkpoint, eventIds, ledgerEvents, vaultBase) {
143
237
  if (!checkpoint || typeof checkpoint !== 'object') return false;
144
238
  if (!Number.isInteger(checkpoint.revision) || checkpoint.revision < 0) return false;
145
239
  if (typeof checkpoint.event_cursor !== 'string' || !checkpoint.event_cursor) return false;
@@ -152,24 +246,32 @@ function checkpointMatchesLedgerPrefix(checkpoint, eventIds, ledgerEvents) {
152
246
  if (eventIds.some((eventId) => !prefixIds.has(eventId))) return false;
153
247
 
154
248
  try {
155
- const replay = reduceMemoryEvents(prefix);
156
- return checkpoint.revision === replay.revision
157
- && checkpoint.event_cursor === replay.eventCursor
158
- && checkpoint.state_hash === replay.stateHash;
249
+ const replay = deriveMemoryProjection(vaultBase, prefix);
250
+ const causalMatches = checkpoint.causal_event_cursor === undefined
251
+ || checkpoint.causal_event_cursor === replay.eventCursor;
252
+ return checkpoint.revision === replay.checkpoint.revision
253
+ && checkpoint.event_cursor === replay.checkpoint.event_cursor
254
+ && checkpoint.state_hash === replay.checkpoint.state_hash
255
+ && causalMatches;
159
256
  } catch {
160
257
  return false;
161
258
  }
162
259
  }
163
260
 
164
- function checkMemoryAttempts(registry, { ledgerEvents = [], outboxEventIds = new Set() } = {}) {
261
+ function checkMemoryAttempts(registry, {
262
+ vaultBase, ledgerEvents = [], outboxEventIds = new Set(), outboxEventsById = new Map(),
263
+ } = {}) {
165
264
  const failures = [];
166
265
  const warnings = [];
167
266
  const ledgerEventIds = new Set(ledgerEvents.map((event) => event?.event_id).filter(Boolean));
168
- const attempts = Object.values(registry?.sessions || {})
169
- .map((entry) => entry?.last_memory_attempt)
170
- .filter((attempt) => attempt && typeof attempt === 'object' && attempt.memory_mode === 'v2');
171
-
172
- for (const attempt of attempts) {
267
+ const ledgerById = new Map(ledgerEvents
268
+ .filter((event) => event?.event_id)
269
+ .map((event) => [event.event_id, event]));
270
+ const attempts = Object.entries(registry?.sessions || {})
271
+ .map(([sessionId, entry]) => [sessionId, entry?.last_memory_attempt])
272
+ .filter(([, attempt]) => attempt && typeof attempt === 'object' && attempt.memory_mode === 'v2');
273
+
274
+ for (const [sessionId, attempt] of attempts) {
173
275
  const state = String(attempt.state || '');
174
276
  const disposition = String(attempt.disposition || '');
175
277
  const eventIds = Array.isArray(attempt.event_ids)
@@ -201,6 +303,37 @@ function checkMemoryAttempts(registry, { ledgerEvents = [], outboxEventIds = new
201
303
  continue;
202
304
  }
203
305
 
306
+ const identity = {
307
+ canonical_session_id: sessionId,
308
+ activation_id: attempt.activation_id,
309
+ activation_epoch: attempt.activation_epoch,
310
+ source_turn_id: attempt.turn_id,
311
+ turn_sequence: attempt.turn_sequence,
312
+ };
313
+ const invalidAttemptFields = [];
314
+ if (attempt.canonical_session_id !== sessionId) invalidAttemptFields.push('canonical_session_id');
315
+ if (typeof attempt.activation_id !== 'string' || !attempt.activation_id) invalidAttemptFields.push('activation_id');
316
+ if (!Number.isInteger(attempt.activation_epoch) || attempt.activation_epoch < 0) invalidAttemptFields.push('activation_epoch');
317
+ if (typeof attempt.turn_id !== 'string' || !attempt.turn_id) invalidAttemptFields.push('turn_id');
318
+ if (!Number.isInteger(attempt.turn_sequence) || attempt.turn_sequence < 0) invalidAttemptFields.push('turn_sequence');
319
+ if (invalidAttemptFields.length) {
320
+ failures.push(`Attempt v2 da sessão ${sessionId} possui identidade causal inválida (${invalidAttemptFields.join(', ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
321
+ continue;
322
+ }
323
+ const causalMismatches = [];
324
+ for (const eventId of eventIds) {
325
+ const event = ledgerById.get(eventId) || outboxEventsById.get(eventId);
326
+ if (!event) continue;
327
+ const fields = Object.entries(identity)
328
+ .filter(([field, expected]) => event[field] !== expected)
329
+ .map(([field]) => field);
330
+ if (fields.length) causalMismatches.push(`${eventId}: ${fields.join(', ')}`);
331
+ }
332
+ if (causalMismatches.length) {
333
+ failures.push(`Attempt v2 da sessão ${sessionId} referencia evento(s) com identidade causal divergente (${causalMismatches.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
334
+ continue;
335
+ }
336
+
204
337
  if (state === 'enqueued' || state === 'degraded') {
205
338
  const missing = eventIds.filter((eventId) => !ledgerEventIds.has(eventId) && !outboxEventIds.has(eventId));
206
339
  if (missing.length) {
@@ -215,7 +348,7 @@ function checkMemoryAttempts(registry, { ledgerEvents = [], outboxEventIds = new
215
348
  const outsideLedger = eventIds.filter((eventId) => !ledgerEventIds.has(eventId));
216
349
  if (outsideLedger.length) {
217
350
  failures.push(`Attempt projetado perdeu ${outsideLedger.length} evento(s) no ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
218
- } else if (!checkpointMatchesLedgerPrefix(attempt.checkpoint, eventIds, ledgerEvents)) {
351
+ } else if (!checkpointMatchesLedgerPrefix(attempt.checkpoint, eventIds, ledgerEvents, vaultBase)) {
219
352
  failures.push(`Checkpoint do attempt projetado diverge do prefixo rederivado do ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
220
353
  }
221
354
  continue;
@@ -232,24 +365,28 @@ function checkMemoryAttempts(registry, { ledgerEvents = [], outboxEventIds = new
232
365
  * does not acquire MEMORY.lock or invoke the projector/repair paths.
233
366
  */
234
367
  export function checkMemoryBundle(vaultBase, { registry } = {}) {
368
+ if (!existsSync(vaultBase)) {
369
+ return {
370
+ ok: false,
371
+ status: 'blocked',
372
+ failures: [`Vault not found: ${vaultBase}`],
373
+ warnings: [],
374
+ metrics: memoryMetrics(),
375
+ };
376
+ }
377
+ try { preflightMemoryBundle(vaultBase); }
378
+ catch (error) { return blockedMemoryBoundary(error); }
235
379
  const brain = join(vaultBase, '.brain');
236
- const mode = detectMemoryMode(vaultBase);
380
+ let mode;
381
+ try { mode = detectMemoryMode(vaultBase); }
382
+ catch (error) { return blockedMemoryBoundary(error); }
237
383
  if (mode.mode === 'legacy') {
238
384
  return {
239
385
  ok: true,
240
386
  status: 'legacy',
241
387
  failures: [],
242
388
  warnings: [LEGACY_MEMORY_WARNING],
243
- metrics: {
244
- schemaVersion: null,
245
- revision: null,
246
- eventCursor: null,
247
- stateHash: null,
248
- ledgerEvents: 0,
249
- pendingOutbox: 0,
250
- candidates: 0,
251
- activeConflicts: 0,
252
- },
389
+ metrics: memoryMetrics(),
253
390
  };
254
391
  }
255
392
  const bundle = validateMemoryBundle(vaultBase);
@@ -260,7 +397,9 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
260
397
  : { metadata: {} };
261
398
  const metadata = parsedShared.metadata || {};
262
399
  const outbox = inspectOutbox(vaultBase, bundle.project?.projectId);
263
- const candidates = readJsonLines(join(brain, 'MEMORY_CANDIDATES.jsonl'), 'MEMORY_CANDIDATES.jsonl');
400
+ const candidates = readJsonLines(
401
+ vaultBase, join(brain, 'MEMORY_CANDIDATES.jsonl'), 'MEMORY_CANDIDATES.jsonl',
402
+ );
264
403
 
265
404
  const ledgerCorrupt = (bundle.ledger?.errors || []).length > 0;
266
405
  if (ledgerCorrupt) {
@@ -282,7 +421,7 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
282
421
 
283
422
  let replay = null;
284
423
  if (bundle.ledger?.ok) {
285
- try { replay = reduceMemoryEvents(bundle.ledger.events); }
424
+ try { replay = deriveMemoryProjection(vaultBase, bundle.ledger.events); }
286
425
  catch (error) {
287
426
  failures.push(`Ledger não pode ser reduzido: ${error.message}. Execute com segurança: ${MEMORY_REPAIR_COMMAND}.`);
288
427
  }
@@ -290,16 +429,23 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
290
429
  if (replay && bundle.shared?.ok) {
291
430
  const divergences = [];
292
431
  if (metadata.revision !== replay.revision) divergences.push(`revision ${metadata.revision} != ${replay.revision}`);
293
- if (metadata.event_cursor !== replay.eventCursor) divergences.push(`event_cursor ${metadata.event_cursor} != ${replay.eventCursor}`);
432
+ if (metadata.event_cursor !== replay.ledgerCursor) divergences.push(`event_cursor ${metadata.event_cursor} != ${replay.ledgerCursor}`);
294
433
  if (metadata.state_hash !== replay.stateHash) divergences.push(`state_hash ${metadata.state_hash} != ${replay.stateHash}`);
295
434
  if (divergences.length) {
296
435
  failures.push(`Projeção SHARED stale/lag (${divergences.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
297
436
  }
298
437
  }
299
438
 
300
- const lifecycle = checkMemoryAttempts(registry || readSessionRegistry(vaultBase), {
439
+ let effectiveRegistry = registry;
440
+ if (!effectiveRegistry) {
441
+ try { effectiveRegistry = readSessionRegistry(vaultBase); }
442
+ catch (error) { failures.push(`SESSION_REGISTRY.json inseguro ou ilegível: ${error?.message || error}.`); }
443
+ }
444
+ const lifecycle = checkMemoryAttempts(effectiveRegistry || { version: 2, sessions: {} }, {
445
+ vaultBase,
301
446
  ledgerEvents: bundle.ledger?.events || [],
302
447
  outboxEventIds: outbox.eventIds,
448
+ outboxEventsById: outbox.eventsById,
303
449
  });
304
450
  failures.push(...lifecycle.failures);
305
451
  warnings.push(...lifecycle.warnings);
@@ -0,0 +1,2 @@
1
+ // Compatibility facade during the physical package migration.
2
+ export * from '../packages/vault/src/vault-path-safety.mjs';