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
@@ -0,0 +1,218 @@
1
+ // Canonical FLOW protection policy. Classification and ignored-file discovery are
2
+ // compiled from the same rules so a protected surface cannot silently disappear
3
+ // merely because Git excludes it from the ordinary worktree status.
4
+ import { pathAllowed } from './git-snapshot.mjs';
5
+ import { realpathSync } from 'node:fs';
6
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
7
+
8
+ const NAME_TOKENS = [
9
+ 'api', 'apis', 'route', 'routes', 'endpoint', 'endpoints',
10
+ 'auth', 'authentication', 'authorization', 'authn', 'authz', 'oauth', 'oidc', 'sso',
11
+ 'security', 'permission', 'permissions', 'role', 'roles', 'rbac', 'abac', 'acl', 'rls',
12
+ 'access', 'policy', 'policies', 'secret', 'secrets', 'credential', 'credentials',
13
+ 'csrf', 'cors', 'webauthn', 'passkey', 'passkeys', 'login', 'logout', 'signin', 'signout',
14
+ 'signup', 'register', 'password', 'passwd', 'jwt', 'token', 'tokens', 'crypto', 'encryption',
15
+ 'decryption', 'keyring', 'keystore', 'release', 'publish', 'publishing', 'deploy', 'deployment',
16
+ ];
17
+ const NAME_TOKEN_SET = new Set(NAME_TOKENS);
18
+ const NAME_DISCOVERY_STEMS = [
19
+ 'api', 'route', 'endpoint', 'auth', 'oidc', 'sso', 'security', 'permission', 'role',
20
+ 'rbac', 'abac', 'acl', 'rls', 'access', 'policy', 'secret', 'credential', 'csrf', 'cors',
21
+ 'webauthn', 'passkey', 'login', 'logout', 'sign', 'register', 'passw', 'jwt', 'token',
22
+ 'crypt', 'key', 'release', 'publish', 'deploy',
23
+ ];
24
+
25
+ export const FLOW_PROTECTED_SCAN_POLICY = Object.freeze({
26
+ schemaVersion: 1,
27
+ maxDepth: 64,
28
+ maxEntries: 100_000,
29
+ excludedDirectoryNames: Object.freeze([
30
+ '.git', 'node_modules', '.pnpm-store', '.yarn', '.venv', 'venv',
31
+ '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', '.nox',
32
+ ]),
33
+ });
34
+
35
+ function nameTokens(path) {
36
+ return String(path || '').replaceAll('\\', '/').split('/').flatMap((segment) => segment
37
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
38
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
39
+ .split(/[^A-Za-z0-9]+/)
40
+ .filter(Boolean)
41
+ .map((token) => token.toLowerCase()));
42
+ }
43
+
44
+ const discoverNames = (stems) => stems.flatMap((stem) => [`**/*${stem}*`, `**/*${stem}*/**`]);
45
+
46
+ function rule(id, test, discoverGlobs, exemplars, topologyRoots = []) {
47
+ return Object.freeze({
48
+ id,
49
+ test,
50
+ discoverGlobs: Object.freeze(discoverGlobs),
51
+ exemplars: Object.freeze(exemplars),
52
+ topologyRoots: Object.freeze(topologyRoots),
53
+ });
54
+ }
55
+
56
+ export const FLOW_PROTECTED_RULES = Object.freeze([
57
+ rule('automation', (path) => /(^|\/)\.github(?:\/|$)/i.test(path)
58
+ || /(^|\/)\.(?:circleci|buildkite|teamcity)(?:\/|$)/i.test(path)
59
+ || /(^|\/)(?:\.gitlab-ci\.ya?ml|\.travis\.ya?ml|appveyor\.ya?ml|cloudbuild\.ya?ml|buildspec\.ya?ml|azure-pipelines\.ya?ml|bitrise\.ya?ml|jenkinsfile|dockerfile(?:\.[^/]+)?|(?:docker-)?compose\.ya?ml)$/i.test(path), [
60
+ '**/.github*', '**/.github*/**', '**/.circleci*', '**/.circleci*/**',
61
+ '**/.buildkite*', '**/.buildkite*/**', '**/.teamcity*', '**/.teamcity*/**', '**/*ci.y*ml',
62
+ '**/*build*', '**/*pipeline*', '**/Jenkinsfile', '**/Dockerfile*', '**/*compose*.y*ml',
63
+ ], ['.github/workflows/ci.yml', 'packages/app/buildspec.yaml'], [
64
+ '.github', '.circleci', '.buildkite', '.teamcity',
65
+ ]),
66
+
67
+ rule('contracts-and-migrations', (path) => /(^|\/)migrations?(?:\/|\.|$)/i.test(path)
68
+ || /(^|\/)(?:db\/migrate|alembic\/versions|flyway\/sql)(?:\/|$)/i.test(path)
69
+ || /(^|\/)(?:schema|schemas|openapi|api-contracts?)(?:\/|\.|$)/i.test(path)
70
+ || /(^|\/)[^/]+\.(?:proto|graphql|gql)$/i.test(path)
71
+ || /(^|\/)(?:prisma|drizzle)(?:\/|$)/i.test(path), [
72
+ ...discoverNames(['migrat', 'schema', 'openapi', 'api-contract', 'prisma', 'drizzle', 'alembic', 'flyway']),
73
+ '**/*.proto', '**/*.graphql', '**/*.gql',
74
+ ], ['db/migrate/001.sql', 'schema.sql', 'contracts/service.proto'], [
75
+ 'migration', 'migrations', 'db/migrate', 'alembic/versions', 'flyway/sql',
76
+ 'schema', 'schemas', 'openapi', 'api-contract', 'api-contracts', 'prisma', 'drizzle',
77
+ ]),
78
+
79
+ rule('dependencies', (path) => /(^|\/)(?:deps?|dependencies|vendor)(?:\/|$)/i.test(path)
80
+ || /(^|\/)(?:package\.json|package-lock\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|yarn\.lock|\.yarnrc(?:\.[^/]+)?|\.npmrc|\.pypirc|bun\.lockb?|deno\.jsonc?|deno\.lock|pyproject\.toml|poetry\.lock|uv\.lock|requirements(?:-[^/]+)?\.txt|cargo\.toml|cargo\.lock|go\.mod|go\.sum|composer\.json|composer\.lock|gemfile|gemfile\.lock|pom\.xml|build\.gradle(?:\.kts)?)$/i.test(path), [
81
+ '**/dep*', '**/dep*/**', '**/vendor*', '**/vendor*/**', '**/package*', '**/pnpm*', '**/yarn*', '**/.npmrc',
82
+ '**/.yarnrc*', '**/.pypirc', '**/*lock*', '**/deno*', '**/pyproject.toml', '**/poetry*',
83
+ '**/requirements*', '**/Cargo*', '**/go.*', '**/composer*', '**/Gemfile*', '**/pom.xml',
84
+ '**/build.gradle*',
85
+ ], ['package.json', 'packages/app/pnpm-lock.yaml', 'vendor/library.js'], [
86
+ 'dep', 'deps', 'dependencies', 'vendor',
87
+ ]),
88
+
89
+ rule('release', (path) => /(^|\/)(?:scripts?\/release(?:\.[^/]+)?|\.changeset(?:\/|$)|changesets?(?:\/|$))/i.test(path)
90
+ || /(^|\/)(?:changelog|release-notes)(?:\.[^/]+)?$/i.test(path)
91
+ || /(^|\/)(?:\.releaserc(?:\.[^/]+)?|release-please-config\.json)$/i.test(path), [
92
+ '**/*release*', '**/*release*/**', '**/.changeset*', '**/.changeset*/**',
93
+ '**/changesets*', '**/changesets*/**', '**/CHANGELOG*', '**/.releaserc*',
94
+ ], ['CHANGELOG.md', '.changeset/release.md'], ['.changeset', 'changesets']),
95
+
96
+ rule('governance', (path) => /(^|\/)(?:07-specs|08-mudan[cç]as|08-changes)(?:\/|$)/i.test(path)
97
+ || /(^|\/)(?:agents|claude)\.md$/i.test(path), [
98
+ '**/07-Specs*', '**/07-Specs*/**', '**/08-Mudanças*', '**/08-Mudanças*/**',
99
+ '**/08-Changes*', '**/08-Changes*/**', '**/AGENTS.md', '**/CLAUDE.md',
100
+ ], ['AGENTS.md', '07-Specs/api.md'], ['07-Specs', '08-Mudanças', '08-Changes']),
101
+
102
+ rule('git-worktree-metadata', (path) => /(^|\/)(?:\.gitignore|\.gitmodules|\.gitattributes)$/i.test(path), [
103
+ '**/.gitignore', '**/.gitmodules', '**/.gitattributes',
104
+ ], ['.gitmodules', 'packages/app/.gitignore']),
105
+
106
+ rule('wendkeep-control-plane', (path) => /(^|\/)\.mcp\.json$/i.test(path)
107
+ || /(^|\/)\.(?:claude|codex)(?:\/|$)/i.test(path)
108
+ || /(^|\/)\.agent\/hooks(?:\/|$)/i.test(path)
109
+ || /(^|\/)\.agents\/skills(?:\/|$)/i.test(path)
110
+ || /(^|\/)(?:wendkeep\.sensors\.json|\.wendkeep\.json)$/i.test(path)
111
+ || /(^|\/)\.(?:agents|claude)\/skills\/wk-[^/]+(?:\/|$)/i.test(path)
112
+ || /(^|\/)hooks(?:\/|$)/i.test(path)
113
+ || /(^|\/)src\/(?:flow|change|verify|sensors|operating-profile|profile|project-vault|memory|init|sync|sync-defs|skills-seed|spec|validate-memory|validate-core)\.mjs$/i.test(path)
114
+ || /(^|\/)bin\/wendkeep\.mjs$/i.test(path), [
115
+ '**/.mcp.json', '**/.claude*', '**/.claude*/**', '**/.codex*', '**/.codex*/**',
116
+ '**/.agent*', '**/.agent*/**', '**/.agents*', '**/.agents*/**',
117
+ '**/.wendkeep.json', '**/wendkeep.sensors.json', '**/hooks/*', '**/src/*', '**/bin/*',
118
+ ], ['.wendkeep.json', 'hooks/flow-core.mjs', '.codex/hooks.json', 'src/memory.mjs'], [
119
+ '.claude', '.codex', '.agent/hooks', '.agents/skills', 'hooks',
120
+ ]),
121
+
122
+ rule('environment', (path) => /(^|\/)\.env(?:\.[^/]+)?$/i.test(path), [
123
+ '**/.env', '**/.env.*',
124
+ ], ['.env.production']),
125
+
126
+ rule('semantic-security-and-api-names', (path) => nameTokens(path)
127
+ .some((token) => NAME_TOKEN_SET.has(token)), discoverNames(NAME_DISCOVERY_STEMS),
128
+ ['src/csrf.ts', 'src/passkeyService.ts', 'src/authn/policy.ts', 'src/APIClient.ts']),
129
+ ]);
130
+
131
+ function asIgnoredPathspec(glob) {
132
+ const value = String(glob || '').trim();
133
+ if (!value) return '';
134
+ if (value.startsWith(':(glob,icase)')) return value;
135
+ if (value.startsWith(':(glob)')) return value.replace(':(glob)', ':(glob,icase)');
136
+ return `:(glob,icase)${value}`;
137
+ }
138
+
139
+ export function isProtectedFlowPath(path, protectedRoots = []) {
140
+ const normalized = String(path || '').replaceAll('\\', '/');
141
+ return FLOW_PROTECTED_RULES.some((entry) => entry.test(normalized))
142
+ || pathAllowed(normalized, protectedRoots);
143
+ }
144
+
145
+ export function flowProtectedIgnoredPathspecs(protectedRoots = []) {
146
+ const customGlobs = (protectedRoots || []).flatMap((root) => {
147
+ const normalized = String(root || '').replaceAll('\\', '/');
148
+ const leaf = normalized.replace(/\/\*\*$/, '');
149
+ return leaf && leaf !== normalized ? [leaf, normalized] : [normalized];
150
+ });
151
+ return [...new Set([
152
+ ...FLOW_PROTECTED_RULES.flatMap((entry) => entry.discoverGlobs),
153
+ ...customGlobs,
154
+ ].map(asIgnoredPathspec).filter(Boolean))].sort();
155
+ }
156
+
157
+ export function flowProtectedPolicyExamples() {
158
+ return FLOW_PROTECTED_RULES.flatMap((entry) => entry.exemplars.map((path) => ({ rule: entry.id, path })));
159
+ }
160
+
161
+ export function flowProtectedTopologyRoots(projectRoot, gitRoot) {
162
+ const physicalProject = realpathSync.native(resolve(projectRoot));
163
+ const physicalGitRoot = realpathSync.native(resolve(gitRoot));
164
+ const rel = relative(physicalGitRoot, physicalProject);
165
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
166
+ throw new TypeError('projectRoot fora da raiz Git ao compilar topologia protegida');
167
+ }
168
+ const prefix = rel ? rel.replaceAll('\\', '/') : '';
169
+ const anchors = FLOW_PROTECTED_RULES.flatMap((entry) => entry.topologyRoots);
170
+ return [...new Set(anchors.flatMap((anchor) => [
171
+ anchor,
172
+ prefix ? `${prefix}/${anchor}` : anchor,
173
+ ]))].sort();
174
+ }
175
+
176
+ function pathInside(root, candidate) {
177
+ const rel = relative(root, candidate);
178
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return '';
179
+ return rel.replaceAll('\\', '/');
180
+ }
181
+
182
+ export function flowProtectedPhysicalScanOptions(projectRoot, gitRoot, {
183
+ protectedRoots = [],
184
+ vaultBase = '',
185
+ } = {}) {
186
+ const physicalProject = realpathSync.native(resolve(projectRoot));
187
+ const physicalGitRoot = realpathSync.native(resolve(gitRoot));
188
+ const fromGit = relative(physicalGitRoot, physicalProject);
189
+ if (fromGit === '..' || fromGit.startsWith(`..${sep}`) || isAbsolute(fromGit)) {
190
+ throw new TypeError('projectRoot fora da raiz Git ao compilar scan físico protegido');
191
+ }
192
+ const pathPrefix = fromGit ? fromGit.replaceAll('\\', '/') : '';
193
+ const physicalVault = vaultBase ? realpathSync.native(resolve(vaultBase)) : '';
194
+ const vaultRel = physicalVault ? pathInside(physicalProject, physicalVault) : '';
195
+ const excludedPaths = vaultRel ? [vaultRel] : [];
196
+ const excludedGitRoots = excludedPaths.map((path) => pathPrefix ? `${pathPrefix}/${path}` : path);
197
+ const excludedNames = new Set(FLOW_PROTECTED_SCAN_POLICY.excludedDirectoryNames
198
+ .map((name) => process.platform === 'win32' ? name.toLowerCase() : name));
199
+ const normalizeCase = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
200
+ const isExcludedPath = (path) => {
201
+ const normalized = normalizeCase(String(path || '').replaceAll('\\', '/'));
202
+ if (normalized.split('/').some((segment) => excludedNames.has(segment))) return true;
203
+ return excludedGitRoots.some((root) => {
204
+ const candidate = normalizeCase(root);
205
+ return normalized === candidate || normalized.startsWith(`${candidate}/`);
206
+ });
207
+ };
208
+ return {
209
+ pathPrefix,
210
+ protectedRoots: [...protectedRoots],
211
+ excludedDirectoryNames: [...FLOW_PROTECTED_SCAN_POLICY.excludedDirectoryNames],
212
+ excludedPaths,
213
+ maxDepth: FLOW_PROTECTED_SCAN_POLICY.maxDepth,
214
+ maxEntries: FLOW_PROTECTED_SCAN_POLICY.maxEntries,
215
+ isProtectedPath: (path) => isProtectedFlowPath(path, protectedRoots),
216
+ isExcludedPath,
217
+ };
218
+ }
@@ -123,7 +123,9 @@ export function repairStackedFrontmatter(vaultBase, { apply = false, lockTimeout
123
123
  }
124
124
 
125
125
  // Sob o mesmo lock dos hooks: reparar enquanto um subagente escreve seria repetir o bug.
126
- const outcome = mutateSessionNote(abs, () => merged, lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {});
126
+ const outcome = mutateSessionNote(abs, () => merged, {
127
+ ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}), vaultBase,
128
+ });
127
129
  if (!outcome.written) {
128
130
  skipped.push({ file: rel, reason: `gravação não ocorreu (${outcome.reason})` });
129
131
  continue;