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.
- package/CHANGELOG.md +120 -0
- package/README.en.md +70 -40
- package/README.md +70 -40
- package/bin/wendkeep.mjs +54 -6
- package/docs/en/commands/changes-and-verification.md +85 -0
- package/docs/en/commands/costs-and-observability.md +65 -0
- package/docs/en/commands/getting-started.md +86 -0
- package/docs/en/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/en/commands/memory-migration.md +73 -0
- package/docs/en/commands/memory.md +102 -0
- package/docs/en/commands/notes-and-knowledge.md +70 -0
- package/docs/en/commands/operating-profiles.md +173 -0
- package/docs/en/commands/retroactive-import.md +67 -0
- package/docs/en/commands/sessions-and-import.md +89 -0
- package/docs/en/commands/verify.md +92 -0
- package/docs/pt-BR/commands/changes-and-verification.md +85 -0
- package/docs/pt-BR/commands/costs-and-observability.md +65 -0
- package/docs/pt-BR/commands/getting-started.md +87 -0
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/pt-BR/commands/memory-migration.md +73 -0
- package/docs/pt-BR/commands/memory.md +99 -0
- package/docs/pt-BR/commands/notes-and-knowledge.md +69 -0
- package/docs/pt-BR/commands/operating-profiles.md +171 -0
- package/docs/pt-BR/commands/retroactive-import.md +67 -0
- package/docs/pt-BR/commands/sessions-and-import.md +89 -0
- package/docs/pt-BR/commands/verify.md +93 -0
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-inject.mjs +83 -26
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-context.mjs +24 -10
- package/hooks/change-core.mjs +174 -37
- package/hooks/change-guard.mjs +115 -16
- package/hooks/change-nag.mjs +20 -5
- package/hooks/change-warn.mjs +27 -9
- package/hooks/decision-capture.mjs +1 -1
- package/hooks/derived-sections.mjs +1 -1
- package/hooks/flow-core.mjs +891 -0
- package/hooks/flow-protected-policy.mjs +218 -0
- package/hooks/frontmatter-repair.mjs +3 -1
- package/hooks/git-snapshot.mjs +722 -0
- package/hooks/import-sessions.mjs +10 -5
- package/hooks/memory-mode.mjs +63 -13
- package/hooks/memory-store.mjs +309 -69
- package/hooks/obsidian-common.mjs +119 -84
- package/hooks/operating-profile-runtime.mjs +157 -0
- package/hooks/plan-capture.mjs +14 -3
- package/hooks/sensors-core.mjs +15 -3
- package/hooks/session-backfill.mjs +7 -2
- package/hooks/session-ensure.mjs +21 -12
- package/hooks/session-iteration.mjs +65 -0
- package/hooks/session-memory-lifecycle.mjs +335 -0
- package/hooks/session-note-io.mjs +130 -15
- package/hooks/session-observability.mjs +4 -2
- package/hooks/session-stop.mjs +181 -59
- package/hooks/spec-core.mjs +91 -12
- package/hooks/subagent-stop.mjs +4 -1
- package/hooks/subagent-usage.mjs +2 -2
- package/hooks/task-log.mjs +3 -1
- package/hooks/token-usage.mjs +1 -1
- package/hooks/vault-health.mjs +268 -25
- package/hooks/vault-path-safety.mjs +558 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +5 -3
- package/src/change.mjs +2 -1
- package/src/flow.mjs +232 -0
- package/src/init.mjs +26 -3
- package/src/memory.mjs +785 -35
- package/src/operating-profile.mjs +133 -0
- package/src/profile.mjs +224 -0
- package/src/project-vault.mjs +110 -5
- package/src/rebuild-costs.mjs +11 -4
- package/src/skills-seed.mjs +38 -16
- package/src/sync-defs.mjs +16 -7
- package/src/sync.mjs +9 -1
- package/src/taxonomy.mjs +9 -0
- package/src/validate-memory.mjs +21 -8
- package/src/verify.mjs +12 -2
package/hooks/change-guard.mjs
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/hooks/change-nag.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
}
|
package/hooks/change-warn.mjs
CHANGED
|
@@ -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 {
|
|
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, {
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 });
|