wendkeep 0.58.1 → 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 (78) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.en.md +70 -40
  3. package/README.md +70 -40
  4. package/bin/wendkeep.mjs +54 -6
  5. package/docs/en/commands/changes-and-verification.md +85 -0
  6. package/docs/en/commands/costs-and-observability.md +65 -0
  7. package/docs/en/commands/getting-started.md +86 -0
  8. package/docs/en/commands/maintenance-and-diagnostics.md +77 -0
  9. package/docs/en/commands/memory-migration.md +73 -0
  10. package/docs/en/commands/memory.md +102 -0
  11. package/docs/en/commands/notes-and-knowledge.md +70 -0
  12. package/docs/en/commands/operating-profiles.md +173 -0
  13. package/docs/en/commands/retroactive-import.md +67 -0
  14. package/docs/en/commands/sessions-and-import.md +89 -0
  15. package/docs/en/commands/verify.md +92 -0
  16. package/docs/pt-BR/commands/changes-and-verification.md +85 -0
  17. package/docs/pt-BR/commands/costs-and-observability.md +65 -0
  18. package/docs/pt-BR/commands/getting-started.md +87 -0
  19. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +77 -0
  20. package/docs/pt-BR/commands/memory-migration.md +73 -0
  21. package/docs/pt-BR/commands/memory.md +99 -0
  22. package/docs/pt-BR/commands/notes-and-knowledge.md +69 -0
  23. package/docs/pt-BR/commands/operating-profiles.md +171 -0
  24. package/docs/pt-BR/commands/retroactive-import.md +67 -0
  25. package/docs/pt-BR/commands/sessions-and-import.md +89 -0
  26. package/docs/pt-BR/commands/verify.md +93 -0
  27. package/hooks/brain-core.mjs +159 -159
  28. package/hooks/brain-inject.mjs +83 -26
  29. package/hooks/brain-recall.mjs +32 -32
  30. package/hooks/brain-reindex.mjs +13 -13
  31. package/hooks/change-context.mjs +24 -10
  32. package/hooks/change-core.mjs +174 -37
  33. package/hooks/change-guard.mjs +115 -16
  34. package/hooks/change-nag.mjs +20 -5
  35. package/hooks/change-warn.mjs +27 -9
  36. package/hooks/decision-capture.mjs +1 -1
  37. package/hooks/derived-sections.mjs +1 -1
  38. package/hooks/flow-core.mjs +891 -0
  39. package/hooks/flow-protected-policy.mjs +218 -0
  40. package/hooks/frontmatter-repair.mjs +3 -1
  41. package/hooks/git-snapshot.mjs +722 -0
  42. package/hooks/import-sessions.mjs +10 -5
  43. package/hooks/memory-mode.mjs +63 -13
  44. package/hooks/memory-store.mjs +309 -69
  45. package/hooks/obsidian-common.mjs +119 -84
  46. package/hooks/operating-profile-runtime.mjs +157 -0
  47. package/hooks/plan-capture.mjs +14 -3
  48. package/hooks/sensors-core.mjs +15 -3
  49. package/hooks/session-backfill.mjs +7 -2
  50. package/hooks/session-ensure.mjs +21 -12
  51. package/hooks/session-iteration.mjs +65 -0
  52. package/hooks/session-memory-lifecycle.mjs +335 -0
  53. package/hooks/session-note-io.mjs +130 -15
  54. package/hooks/session-observability.mjs +4 -2
  55. package/hooks/session-stop.mjs +181 -59
  56. package/hooks/spec-core.mjs +91 -12
  57. package/hooks/subagent-stop.mjs +4 -1
  58. package/hooks/subagent-usage.mjs +2 -2
  59. package/hooks/task-log.mjs +3 -1
  60. package/hooks/token-usage.mjs +1 -1
  61. package/hooks/vault-health.mjs +268 -25
  62. package/hooks/vault-path-safety.mjs +558 -0
  63. package/hooks/vault-runtime-store.mjs +558 -0
  64. package/package.json +5 -3
  65. package/src/change.mjs +2 -1
  66. package/src/flow.mjs +232 -0
  67. package/src/init.mjs +26 -3
  68. package/src/memory.mjs +785 -35
  69. package/src/operating-profile.mjs +133 -0
  70. package/src/profile.mjs +224 -0
  71. package/src/project-vault.mjs +110 -5
  72. package/src/rebuild-costs.mjs +11 -4
  73. package/src/skills-seed.mjs +38 -16
  74. package/src/sync-defs.mjs +16 -7
  75. package/src/sync.mjs +9 -1
  76. package/src/taxonomy.mjs +9 -0
  77. package/src/validate-memory.mjs +21 -8
  78. package/src/verify.mjs +12 -2
@@ -5,21 +5,105 @@
5
5
  // no ambiente do processo — env inline no texto do comando NÃO conta).
6
6
  // R2 — `git commit` com change ativa E (--no-verify OU sensor crítico vermelho) vira `ask`
7
7
  // (o usuário decide com 1 clique; falso-positivo custa pouco).
8
- // Fast-path: comando sem wendkeep/wk/git sai sem NENHUM I/O. Fail-open.
8
+ // Fast-path: comando sem wendkeep/wk/git sai sem NENHUM I/O. Ausência normal continua
9
+ // fail-open; corrupção do binding é diagnóstico visível e fail-closed.
9
10
  import { pathToFileURL } from 'node:url';
10
- import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
11
+ import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
11
12
  import { activeChange, quickGateState } from './change-core.mjs';
13
+ import { hookProfilePolicy, resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
14
+ import { isProjectVaultIntegrityError } from '../src/project-vault.mjs';
12
15
 
13
- const FORCE_RE = /\b(?:wendkeep|wk)\s+change\s+archive\b[^|&;\n]*--force\b/;
14
- const GIT_SEG_RE = /(^|&&|;|\|)\s*git\b[^|&;]*\bcommit\b/;
15
- const FAST_RE = /\b(?:wendkeep|wk|git)\b/;
16
+ const WK_EXECUTABLES = new Set([
17
+ 'wendkeep', 'wendkeep.cmd', 'wendkeep.exe', 'wendkeep.ps1', 'wendkeep.mjs',
18
+ 'wk', 'wk.cmd', 'wk.exe', 'wk.ps1',
19
+ ]);
20
+ const NODE_EXECUTABLES = new Set(['node', 'node.exe']);
21
+ const NPX_EXECUTABLES = new Set(['npx', 'npx.cmd', 'npx.exe']);
22
+ const GIT_EXECUTABLES = new Set(['git', 'git.exe', 'git.cmd']);
16
23
 
17
- export function guardDecision(command, { vaultBase, env = process.env } = {}) {
24
+ function shellSegments(command) {
25
+ const tokens = String(command || '').match(/"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|&&|\|\||[;|\n]|&|[^\s;&|]+/g) || [];
26
+ const segments = [];
27
+ let current = [];
28
+ const flush = () => {
29
+ if (current.length) segments.push(current);
30
+ current = [];
31
+ };
32
+ for (const token of tokens) {
33
+ if (['&&', '||', ';', '|', '\n'].includes(token) || (token === '&' && current.length)) {
34
+ flush();
35
+ continue;
36
+ }
37
+ current.push(token);
38
+ }
39
+ flush();
40
+ return segments;
41
+ }
42
+
43
+ function unquote(token) {
44
+ const value = String(token || '');
45
+ if (value.length >= 2 && ((value[0] === '"' && value.at(-1) === '"')
46
+ || (value[0] === "'" && value.at(-1) === "'"))) return value.slice(1, -1);
47
+ return value;
48
+ }
49
+
50
+ function executableName(token) {
51
+ return unquote(token).replaceAll('\\', '/').split('/').at(-1).toLowerCase();
52
+ }
53
+
54
+ function invocationOf(segment) {
55
+ let index = 0;
56
+ while (segment[index] === '&' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[index] || '')) index += 1;
57
+ const executable = executableName(segment[index]);
58
+ if (WK_EXECUTABLES.has(executable)) {
59
+ return { kind: 'wendkeep', args: segment.slice(index + 1).map(unquote) };
60
+ }
61
+ if (NODE_EXECUTABLES.has(executable)) {
62
+ let scriptIndex = index + 1;
63
+ while (String(segment[scriptIndex] || '').startsWith('-')) scriptIndex += 1;
64
+ if (executableName(segment[scriptIndex]) === 'wendkeep.mjs') {
65
+ return { kind: 'wendkeep', args: segment.slice(scriptIndex + 1).map(unquote) };
66
+ }
67
+ }
68
+ if (NPX_EXECUTABLES.has(executable)) {
69
+ let packageIndex = index + 1;
70
+ while (String(segment[packageIndex] || '').startsWith('-')) packageIndex += 1;
71
+ if (WK_EXECUTABLES.has(executableName(segment[packageIndex]))) {
72
+ return { kind: 'wendkeep', args: segment.slice(packageIndex + 1).map(unquote) };
73
+ }
74
+ }
75
+ if (GIT_EXECUTABLES.has(executable)) {
76
+ return { kind: 'git', args: segment.slice(index + 1).map(unquote) };
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function commandInvocations(command) {
82
+ return shellSegments(command).map(invocationOf).filter(Boolean);
83
+ }
84
+
85
+ function bindingFailureDecision(diagnostic) {
86
+ const code = diagnostic?.code || 'WENDKEEP_VAULT_CONFIG_INVALID';
87
+ const raw = diagnostic?.message || String(diagnostic || 'Configuração WendKeep inválida.');
88
+ const detail = raw.replace(/\s+/g, ' ').trim().slice(0, 420);
89
+ return {
90
+ permissionDecision: 'deny',
91
+ permissionDecisionReason: `${code}: ${detail} Corrija o binding antes de executar uma ação mutável.`,
92
+ };
93
+ }
94
+
95
+ export function guardDecision(command, { vaultBase, env = process.env, profile = 'GOVERN' } = {}) {
96
+ if (!hookProfilePolicy(profile).harness) return null;
18
97
  const cmd = String(command || '');
19
- if (!FAST_RE.test(cmd)) return null; // fast-path: zero I/O para o caso comum
98
+ const invocations = commandInvocations(cmd);
99
+ if (!invocations.length) return null; // fast-path: parsing puro, zero I/O para o caso comum
20
100
 
21
- // R1: archive --force — puro regex, ainda sem I/O. Reason fala com o AGENTE (deny).
22
- if (FORCE_RE.test(cmd)) {
101
+ // R1: archive --force — parser puro, ainda sem I/O. Reason fala com o AGENTE (deny).
102
+ const forcedArchive = invocations.find(({ kind, args }) => kind === 'wendkeep'
103
+ && args[0]?.toLowerCase() === 'change'
104
+ && args[1]?.toLowerCase() === 'archive'
105
+ && args.some((arg) => /^--force(?:=|$)/i.test(arg)));
106
+ if (forcedArchive) {
23
107
  if (env.WENDKEEP_ALLOW_FORCE === '1') return null;
24
108
  return {
25
109
  permissionDecision: 'deny',
@@ -28,12 +112,12 @@ export function guardDecision(command, { vaultBase, env = process.env } = {}) {
28
112
  }
29
113
 
30
114
  // R2: git commit — 1ª leitura de fs só acontece aqui. Reason fala com o USUÁRIO (ask).
31
- const m = cmd.match(GIT_SEG_RE);
32
- if (m) {
115
+ const gitCommit = invocations.find(({ kind, args }) => kind === 'git'
116
+ && args.some((arg) => arg.toLowerCase() === 'commit'));
117
+ if (gitCommit) {
33
118
  const slug = activeChange(vaultBase);
34
119
  if (!slug) return null;
35
- const seg = cmd.slice(m.index + m[1].length).split(/&&|;|\|/)[0];
36
- const noVerify = /\s--no-verify\b/.test(seg);
120
+ const noVerify = gitCommit.args.some((arg) => /^--no-verify(?:=|$)/i.test(arg));
37
121
  const gate = noVerify ? null : quickGateState(vaultBase);
38
122
  if (noVerify || (gate && gate.redCritical)) {
39
123
  return {
@@ -50,10 +134,25 @@ export function guardDecision(command, { vaultBase, env = process.env } = {}) {
50
134
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
51
135
  try {
52
136
  const input = readHookInput();
53
- const d = guardDecision(input.tool_input?.command, { vaultBase: getVaultBase(input) });
137
+ const runtime = resolveHookOperatingProfile({ input });
138
+ const d = runtime.bindingError
139
+ ? bindingFailureDecision(runtime.bindingError)
140
+ : guardDecision(input.tool_input?.command, {
141
+ vaultBase: runtime.vaultBase,
142
+ profile: runtime.profile,
143
+ });
54
144
  if (d) writeHookOutput({ hookSpecificOutput: { hookEventName: 'PreToolUse', ...d } });
55
145
  // allow implícito: exit 0 sem output
56
- } catch {
57
- writeHookOutput({}); // fail-open = allow
146
+ } catch (error) {
147
+ if (isProjectVaultIntegrityError(error)) {
148
+ writeHookOutput({
149
+ hookSpecificOutput: {
150
+ hookEventName: 'PreToolUse',
151
+ ...bindingFailureDecision(error),
152
+ },
153
+ });
154
+ } else {
155
+ writeHookOutput({}); // ausência/erro não-corrupto preserva compatibilidade fail-open
156
+ }
58
157
  }
59
158
  }
@@ -5,16 +5,25 @@
5
5
  // ela o modelo é incentivado a marcar done falso só para conseguir parar).
6
6
  // Anti-loop absoluto: stop_hook_active é o PRIMEIRO check, antes de qualquer I/O.
7
7
  import { pathToFileURL } from 'node:url';
8
- import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
8
+ import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
9
+ import { profileRuntimeError } from './brain-inject.mjs';
9
10
  import { quickGateState, readSentinel, writeSentinel } from './change-core.mjs';
11
+ import {
12
+ hookProfilePolicy,
13
+ profileSentinelId,
14
+ resolveHookOperatingProfile,
15
+ } from './operating-profile-runtime.mjs';
10
16
 
11
- export function nagDecision(input, vaultBase) {
17
+ export function nagDecision(input, vaultBase, { profile = 'GOVERN' } = {}) {
12
18
  if (input && input.stop_hook_active) return null; // anti-loop: sempre primeiro
19
+ const policy = hookProfilePolicy(profile);
20
+ if (!policy.harness) return null;
13
21
  const gate = quickGateState(vaultBase);
14
22
  if (!gate || !gate.openTasks) return null;
15
23
  const sid = input?.session_id || input?.sessionId || '';
16
- if (readSentinel(vaultBase, 'nag', sid)) return null;
17
- writeSentinel(vaultBase, 'nag', sid);
24
+ const sentinelId = profileSentinelId(sid, profile);
25
+ if (readSentinel(vaultBase, 'nag', sentinelId)) return null;
26
+ writeSentinel(vaultBase, 'nag', sentinelId);
18
27
  return {
19
28
  decision: 'block',
20
29
  reason: `A change ativa "${gate.slug}" tem ${gate.openTasks} tarefa(s) aberta(s). Antes de encerrar: marque as concluídas com \`wendkeep change done <id>\`, rode \`wendkeep verify\`, ou informe a pendência ao usuário e encerre.`,
@@ -24,7 +33,13 @@ export function nagDecision(input, vaultBase) {
24
33
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
25
34
  try {
26
35
  const input = readHookInput();
27
- writeHookOutput(nagDecision(input, getVaultBase(input)) || {});
36
+ const runtime = resolveHookOperatingProfile({ input });
37
+ const decision = input?.stop_hook_active
38
+ ? null
39
+ : runtime.bindingError
40
+ ? { decision: 'block', reason: profileRuntimeError(runtime.bindingError) }
41
+ : nagDecision(input, runtime.vaultBase, { profile: runtime.profile });
42
+ writeHookOutput(decision || {});
28
43
  } catch {
29
44
  writeHookOutput({});
30
45
  }
@@ -4,8 +4,14 @@
4
4
  // bloqueia — mudança trivial pode seguir. Ignora edições no vault e em .claude/.agent/.brain.
5
5
  import { isAbsolute, resolve } from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
- import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
7
+ import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
8
+ import { profileRuntimeError } from './brain-inject.mjs';
8
9
  import { activeChange, readSentinel, writeSentinel } from './change-core.mjs';
10
+ import {
11
+ hookProfilePolicy,
12
+ profileSentinelId,
13
+ resolveHookOperatingProfile,
14
+ } from './operating-profile-runtime.mjs';
9
15
 
10
16
  const CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|prisma|sql|go|rs|java|cs)$/i;
11
17
 
@@ -16,15 +22,23 @@ export function isCodeFile(p) {
16
22
  const norm = (p) => String(p || '').replace(/\\/g, '/');
17
23
 
18
24
  // Retorna o additionalContext do aviso, ou null (caso comum).
19
- export function warnDecision(filePath, { vaultBase, cwd = '.', sessionId = '' } = {}) {
25
+ export function warnDecision(filePath, {
26
+ vaultBase,
27
+ cwd = '.',
28
+ sessionId = '',
29
+ profile = 'GOVERN',
30
+ } = {}) {
31
+ const policy = hookProfilePolicy(profile);
32
+ if (!policy.requiresChange) return null;
20
33
  if (!filePath || !isCodeFile(filePath)) return null;
21
34
  if (activeChange(vaultBase)) return null;
22
35
  const abs = norm(isAbsolute(filePath) ? filePath : resolve(cwd, filePath));
23
36
  // Dentro do vault (NTFS é case-insensitive) ou em dirs de config de agente: não é código do projeto.
24
37
  if (abs.toLowerCase().startsWith(`${norm(resolve(vaultBase)).toLowerCase()}/`)) return null;
25
38
  if (/\/(\.claude|\.agent|\.brain)\//.test(`${abs}/`)) return null;
26
- if (readSentinel(vaultBase, 'warn', sessionId)) return null;
27
- writeSentinel(vaultBase, 'warn', sessionId);
39
+ const sentinelId = profileSentinelId(sessionId, profile);
40
+ if (readSentinel(vaultBase, 'warn', sentinelId)) return null;
41
+ writeSentinel(vaultBase, 'warn', sentinelId);
28
42
  return [
29
43
  '<change_warn>',
30
44
  `Você editou código (${filePath}) sem change ativa. Para trabalho não-trivial, roteie pelo processo: \`wendkeep change new <slug>\` e preencha proposta/design/tarefas (skill wk-workflow). Ignore se for um ajuste trivial — este aviso não repete nesta sessão.`,
@@ -35,11 +49,15 @@ export function warnDecision(filePath, { vaultBase, cwd = '.', sessionId = '' }
35
49
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
36
50
  try {
37
51
  const input = readHookInput();
38
- const warn = warnDecision(input.tool_input?.file_path, {
39
- vaultBase: getVaultBase(input),
40
- cwd: input.cwd || '.',
41
- sessionId: input.session_id || input.sessionId || '',
42
- });
52
+ const runtime = resolveHookOperatingProfile({ input });
53
+ const warn = runtime.bindingError
54
+ ? profileRuntimeError(runtime.bindingError)
55
+ : warnDecision(input.tool_input?.file_path, {
56
+ vaultBase: runtime.vaultBase,
57
+ cwd: input.cwd || '.',
58
+ sessionId: input.session_id || input.sessionId || '',
59
+ profile: runtime.profile,
60
+ });
43
61
  if (!warn) { writeHookOutput({}); }
44
62
  else writeHookOutput({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: warn } });
45
63
  } catch {
@@ -212,7 +212,7 @@ export function captureDecision(vaultBase, input) {
212
212
  session = anchor === -1 ? `${session.trimEnd()}${section}` : `${session.slice(0, anchor).trimEnd()}${section}${session.slice(anchor)}`;
213
213
  }
214
214
  return session;
215
- });
215
+ }, { vaultBase });
216
216
  } catch { /* backlink auxiliar nunca derruba a captura */ }
217
217
  }
218
218
  return { rel, skipped: false };
@@ -178,7 +178,7 @@ export function repairDerivedSections(vaultBase, { apply = false, lockTimeoutMs
178
178
  const outcome = mutateSessionNote(
179
179
  abs,
180
180
  (original) => applyDerivedSections(original, mergedForNote(entry)),
181
- lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {},
181
+ { ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}), vaultBase },
182
182
  );
183
183
  if (!outcome.written) { skipped.push({ file: rel, reason: `gravação não ocorreu (${outcome.reason})` }); continue; }
184
184
  repaired.push({ file: rel, missing });