wendkeep 0.58.3 → 0.59.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 (68) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.en.md +41 -3
  3. package/README.md +41 -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 +558 -0
  53. package/hooks/vault-runtime-store.mjs +558 -0
  54. package/package.json +3 -3
  55. package/src/change.mjs +2 -1
  56. package/src/flow.mjs +232 -0
  57. package/src/init.mjs +26 -3
  58. package/src/memory.mjs +785 -35
  59. package/src/operating-profile.mjs +133 -0
  60. package/src/profile.mjs +224 -0
  61. package/src/project-vault.mjs +110 -5
  62. package/src/rebuild-costs.mjs +11 -4
  63. package/src/skills-seed.mjs +38 -16
  64. package/src/sync-defs.mjs +16 -7
  65. package/src/sync.mjs +9 -1
  66. package/src/taxonomy.mjs +8 -0
  67. package/src/validate-memory.mjs +21 -8
  68. package/src/verify.mjs +12 -2
@@ -245,12 +245,12 @@ export function importSession(vaultBase, txPath, opts = {}) {
245
245
  // One iteration block per turn (insertIteration dedups by turn marker -> re-import safe).
246
246
  for (const turn of turns) {
247
247
  const block = buildIterationBlock(tx, { turn_id: turn.turnId, now: turn.timestamp });
248
- insertIteration(absPath, block, turn.turnId, tx);
248
+ insertIteration(absPath, block, turn.turnId, tx, vaultBase);
249
249
  }
250
250
 
251
251
  // Cost + subagent telemetry, exactly like the live Stop hook. Fail-open.
252
252
  try {
253
- updateSessionObservability({ sessionPath: absPath, transcriptPath: txPath });
253
+ updateSessionObservability({ vaultBase, sessionPath: absPath, transcriptPath: txPath });
254
254
  } catch { /* observability is best-effort */ }
255
255
 
256
256
  // Finalize: derived notes + closing section + ended_at from the last turn.
@@ -259,7 +259,7 @@ export function importSession(vaultBase, txPath, opts = {}) {
259
259
  createLinkedNotes(vaultBase, formatDate(endDate), relPath, tx),
260
260
  findLinkedDerivedNotes(vaultBase, relPath),
261
261
  );
262
- finalizeSessionFile(absPath, tx, created, endedAt);
262
+ finalizeSessionFile(absPath, tx, created, endedAt, vaultBase);
263
263
 
264
264
  upsertSessionRegistry(vaultBase, sessionId, {
265
265
  session_file: relPath,
@@ -399,9 +399,14 @@ export function runImport(vaultBase, opts = {}) {
399
399
  }
400
400
  try {
401
401
  for (const turn of missing) {
402
- insertIteration(existingNote, buildIterationBlock(tx, { turn_id: turn.turnId, now: turn.timestamp }), turn.turnId, tx);
402
+ insertIteration(
403
+ existingNote, buildIterationBlock(tx, { turn_id: turn.turnId, now: turn.timestamp }),
404
+ turn.turnId, tx, vaultBase,
405
+ );
403
406
  }
404
- try { updateSessionObservability({ sessionPath: existingNote, transcriptPath: t.path }); } catch { /* best-effort */ }
407
+ try {
408
+ updateSessionObservability({ vaultBase, sessionPath: existingNote, transcriptPath: t.path });
409
+ } catch { /* best-effort */ }
405
410
  report.sessions.push({ sessionId: t.sessionId, turns: missing.length, repaired: true });
406
411
  report.repaired++;
407
412
  done++;
@@ -1,37 +1,87 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
4
  import { classifySharedMemory } from './memory-schema.mjs';
5
+ import { assertVaultPathSafe } from './vault-path-safety.mjs';
5
6
 
6
7
  export const LEGACY_MEMORY_WARNING = 'Vault legado: CORE+DIGEST permanece ativo; execute `wendkeep memory migrate --apply` quando a curadoria estiver pronta.';
7
8
 
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 }; }
9
+ function aliasBoundaryError(error) {
10
+ return error?.code === 'VAULT_PATH_UNSAFE'
11
+ && /link simbólico|junction|reparse|hardlink|nlink|redirecion|escapa logicamente/i
12
+ .test(String(error?.message || error));
12
13
  }
13
14
 
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; }
15
+ function readText(vaultBase, path, label) {
16
+ try {
17
+ let checked = assertVaultPathSafe(vaultBase, path, { expectedType: 'file', label });
18
+ if (!checked.exists) return { exists: false, content: '', error: null };
19
+ checked = assertVaultPathSafe(vaultBase, checked.target, {
20
+ allowMissing: false, expectedType: 'file', label,
21
+ });
22
+ return { exists: true, content: readFileSync(checked.target, 'utf8'), error: null };
23
+ }
24
+ catch (error) {
25
+ if (aliasBoundaryError(error)) throw error;
26
+ return { exists: true, content: '', error };
27
+ }
28
+ }
29
+
30
+ function hasOutboxEvidence(vaultBase, path) {
31
+ try {
32
+ let checked = assertVaultPathSafe(vaultBase, path, {
33
+ expectedType: 'directory', label: 'outbox de memória',
34
+ });
35
+ if (!checked.exists) return false;
36
+ checked = assertVaultPathSafe(vaultBase, checked.target, {
37
+ allowMissing: false, expectedType: 'directory', label: 'outbox de memória',
38
+ });
39
+ const entries = readdirSync(checked.target, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ assertVaultPathSafe(vaultBase, join(checked.target, entry.name), {
42
+ allowMissing: false, label: `entrada ${entry.name} da outbox de memória`,
43
+ });
44
+ }
45
+ return entries.some((entry) => entry.isFile());
46
+ }
47
+ catch (error) {
48
+ if (aliasBoundaryError(error)) throw error;
49
+ return true;
50
+ }
51
+ }
52
+
53
+ function preflightOptionalMemoryFile(vaultBase, path, label) {
54
+ try { assertVaultPathSafe(vaultBase, path, { expectedType: 'file', label }); }
55
+ catch (error) {
56
+ if (aliasBoundaryError(error)) throw error;
57
+ // Ordinary unreadable/wrong-type layers preserve the existing degraded-mode contract.
58
+ }
18
59
  }
19
60
 
20
61
  /** A single, read-only mode decision shared by injection, health and SessionStop. */
21
62
  export function detectMemoryMode(vaultBase) {
22
63
  const brain = join(vaultBase, '.brain');
23
- const shared = readText(join(brain, 'SHARED_MEMORY.md'));
64
+ assertVaultPathSafe(vaultBase, brain, {
65
+ expectedType: 'directory', label: 'raiz .brain da memória',
66
+ });
67
+ // Preflight the layers consumed immediately after mode detection by injection and Stop.
68
+ for (const name of ['CORE.md', 'PROJECT.json', 'DIGEST.md']) {
69
+ preflightOptionalMemoryFile(vaultBase, join(brain, name), `camada de memória ${name}`);
70
+ }
71
+ const shared = readText(vaultBase, join(brain, 'SHARED_MEMORY.md'), 'camada SHARED_MEMORY.md');
24
72
  if (shared.error) return { mode: 'v2', reason: 'shared-unreadable' };
25
73
  const classified = classifySharedMemory(shared.content);
26
74
  if (classified.mode === 'v2') return classified;
27
75
 
28
- const ledger = readText(join(brain, 'MEMORY_EVENTS.jsonl'));
76
+ const ledger = readText(vaultBase, join(brain, 'MEMORY_EVENTS.jsonl'), 'ledger MEMORY_EVENTS.jsonl');
29
77
  if (ledger.error) return { mode: 'v2', reason: 'ledger-unreadable' };
30
- const candidates = readText(join(brain, 'MEMORY_CANDIDATES.jsonl'));
78
+ const candidates = readText(
79
+ vaultBase, join(brain, 'MEMORY_CANDIDATES.jsonl'), 'sidecar MEMORY_CANDIDATES.jsonl',
80
+ );
31
81
  if (candidates.error) return { mode: 'v2', reason: 'candidates-unreadable' };
32
82
  const ledgerHasEvents = ledger.content.trim().length > 0;
33
83
  const candidatesHaveEntries = candidates.content.trim().length > 0;
34
- const outboxHasEvents = hasOutboxEvidence(join(brain, 'memory-outbox'));
84
+ const outboxHasEvents = hasOutboxEvidence(vaultBase, join(brain, 'memory-outbox'));
35
85
  if (ledgerHasEvents || candidatesHaveEntries || outboxHasEvents) {
36
86
  return { mode: 'v2', reason: 'operational-evidence' };
37
87
  }