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
@@ -0,0 +1,133 @@
1
+ export const OPERATING_PROFILES = Object.freeze([
2
+ 'OFF',
3
+ 'FLOW',
4
+ 'GUIDE',
5
+ 'GOVERN',
6
+ 'ASSURE',
7
+ ]);
8
+ export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
9
+
10
+ const PROFILE_SET = new Set(OPERATING_PROFILES);
11
+
12
+ function policy(profile, route, options) {
13
+ return Object.freeze({
14
+ profile,
15
+ route: Object.freeze(route),
16
+ keepCore: true,
17
+ ...options,
18
+ });
19
+ }
20
+
21
+ export const OPERATING_PROFILE_POLICIES = Object.freeze({
22
+ OFF: policy('OFF', ['LLM'], {
23
+ harness: false,
24
+ contract: 'native',
25
+ requiresChange: false,
26
+ requiresReview: false,
27
+ requiresConfirmation: false,
28
+ }),
29
+ FLOW: policy('FLOW', ['E', 'V'], {
30
+ harness: true,
31
+ contract: 'flow',
32
+ requiresChange: false,
33
+ requiresReview: false,
34
+ requiresConfirmation: false,
35
+ }),
36
+ GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
37
+ harness: true,
38
+ contract: 'simple-change',
39
+ requiresChange: true,
40
+ requiresReview: false,
41
+ requiresConfirmation: false,
42
+ }),
43
+ GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
44
+ harness: true,
45
+ contract: 'change',
46
+ requiresChange: true,
47
+ requiresReview: true,
48
+ requiresConfirmation: false,
49
+ }),
50
+ ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
51
+ harness: true,
52
+ contract: 'change',
53
+ requiresChange: true,
54
+ requiresReview: true,
55
+ requiresConfirmation: true,
56
+ }),
57
+ });
58
+
59
+ function invalidProfileError(value) {
60
+ const rendered = typeof value === 'string' ? `"${value}"` : String(value);
61
+ const error = new Error(
62
+ `Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
63
+ );
64
+ error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
65
+ return error;
66
+ }
67
+
68
+ function canonicalProfile(value) {
69
+ if (typeof value !== 'string') return '';
70
+ return value.trim().toUpperCase();
71
+ }
72
+
73
+ export function normalizeOperatingProfile(value, { strict = false } = {}) {
74
+ const normalized = canonicalProfile(value);
75
+ if (PROFILE_SET.has(normalized)) return normalized;
76
+ if (strict) throw invalidProfileError(value);
77
+ return DEFAULT_OPERATING_PROFILE;
78
+ }
79
+
80
+ export function resolveOperatingProfile(config = {}) {
81
+ const harness = config && typeof config === 'object' && !Array.isArray(config)
82
+ && config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
83
+ ? config.harness
84
+ : null;
85
+ const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
86
+ if (!configured) {
87
+ return {
88
+ profile: DEFAULT_OPERATING_PROFILE,
89
+ source: 'default',
90
+ valid: true,
91
+ configured: false,
92
+ raw: null,
93
+ };
94
+ }
95
+
96
+ const raw = harness.profile;
97
+ const normalized = canonicalProfile(raw);
98
+ if (PROFILE_SET.has(normalized)) {
99
+ return {
100
+ profile: normalized,
101
+ source: 'project-binding',
102
+ valid: true,
103
+ configured: true,
104
+ raw,
105
+ };
106
+ }
107
+ return {
108
+ profile: DEFAULT_OPERATING_PROFILE,
109
+ source: 'default-invalid',
110
+ valid: false,
111
+ configured: true,
112
+ raw,
113
+ };
114
+ }
115
+
116
+ export function operatingProfilePolicy(value) {
117
+ return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
118
+ }
119
+
120
+ export function setOperatingProfile(config = {}, value) {
121
+ const profile = normalizeOperatingProfile(value, { strict: true });
122
+ const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
123
+ const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
124
+ ? base.harness
125
+ : {};
126
+ return {
127
+ ...base,
128
+ harness: {
129
+ ...harness,
130
+ profile,
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,224 @@
1
+ // Public operating-profile CLI. Keep project binding and session override mutations atomic,
2
+ // while profile policy itself remains pure in operating-profile.mjs.
3
+ import { mutateSessionRegistry, readSessionRegistry } from '../hooks/obsidian-common.mjs';
4
+ import {
5
+ DEFAULT_OPERATING_PROFILE,
6
+ normalizeOperatingProfile,
7
+ resolveOperatingProfile,
8
+ setOperatingProfile,
9
+ } from './operating-profile.mjs';
10
+ import { resolve } from 'node:path';
11
+ import { findProjectBinding, resolveProjectVault, updateProjectBinding } from './project-vault.mjs';
12
+
13
+ export const PROFILE_HELP = `wendkeep profile <subcommand>
14
+
15
+ status [--session <id>]
16
+ use <OFF|FLOW|GUIDE|GOVERN|ASSURE> [--session <id>]
17
+
18
+ Common options: --project <path> --vault <path> --session <id> --json
19
+ The Keep Core (Vault, session, and memory) remains active under every profile.
20
+ `;
21
+
22
+ const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
23
+ const FLAG_OPTIONS = new Set(['--json']);
24
+
25
+ function optionValue(argv, name) {
26
+ const index = argv.indexOf(name);
27
+ if (index >= 0) return argv[index + 1] || '';
28
+ return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
29
+ }
30
+
31
+ function commandArgs(argv) {
32
+ const values = [];
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const value = argv[index];
35
+ if (['--project', '--vault', '--session'].includes(value)) { index += 1; continue; }
36
+ if (value.startsWith('--project=') || value.startsWith('--vault=') || value.startsWith('--session=')) continue;
37
+ if (value === '--json') continue;
38
+ values.push(value);
39
+ }
40
+ return values;
41
+ }
42
+
43
+ function validateArgv(argv) {
44
+ const seen = new Set();
45
+ for (let index = 0; index < argv.length; index += 1) {
46
+ const value = argv[index];
47
+ if (FLAG_OPTIONS.has(value)) {
48
+ if (seen.has(value)) throw new Error(`opção duplicada: ${value}`);
49
+ seen.add(value);
50
+ continue;
51
+ }
52
+ if (VALUE_OPTIONS.has(value)) {
53
+ if (seen.has(value)) throw new Error(`opção duplicada: ${value}`);
54
+ seen.add(value);
55
+ const next = argv[index + 1];
56
+ if (!next || next.startsWith('--')) throw new Error(`${value} requer um valor`);
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (value.startsWith('--')) {
61
+ const name = value.split('=', 1)[0];
62
+ if (!VALUE_OPTIONS.has(name)) throw new Error(`opção desconhecida: ${name}`);
63
+ if (seen.has(name)) throw new Error(`opção duplicada: ${name}`);
64
+ seen.add(name);
65
+ const inlineValue = value.slice(name.length + 1);
66
+ if (!inlineValue || inlineValue.startsWith('--')) throw new Error(`${name} requer um valor`);
67
+ }
68
+ }
69
+ }
70
+
71
+ function canonicalPath(value) {
72
+ const path = resolve(value).replaceAll('\\', '/');
73
+ return process.platform === 'win32' ? path.toLowerCase() : path;
74
+ }
75
+
76
+ function output(payload, json) {
77
+ if (json) process.stdout.write(`${JSON.stringify(payload)}\n`);
78
+ else {
79
+ const scope = payload.scope === 'session' ? `session ${payload.session_id}` : 'project';
80
+ process.stdout.write(`${payload.profile} (${scope}; ${payload.source})\n`);
81
+ }
82
+ if (payload.binding_error) {
83
+ const code = payload.binding_error.code || 'WENDKEEP_VAULT_CONFIG_INVALID';
84
+ process.stderr.write(`wendkeep profile: ${code}: ${payload.binding_error.message || 'binding WendKeep inválido'}\n`);
85
+ }
86
+ }
87
+
88
+ function fail(message) {
89
+ process.stderr.write(`wendkeep profile: ${message}\n`);
90
+ return 2;
91
+ }
92
+
93
+ function context(argv) {
94
+ const explicitVault = optionValue(argv, '--vault');
95
+ const startDir = optionValue(argv, '--project') || process.cwd();
96
+ const resolved = resolveProjectVault({ startDir, explicitVault });
97
+ let binding = null;
98
+ try { binding = findProjectBinding(startDir); }
99
+ catch (error) {
100
+ if (!explicitVault) throw error;
101
+ }
102
+ const matchingBinding = binding && canonicalPath(binding.base) === canonicalPath(resolved.base) ? binding : null;
103
+ const projectConfig = resolved.config || matchingBinding?.config || {};
104
+ return {
105
+ resolved: {
106
+ ...resolved,
107
+ projectRoot: matchingBinding?.projectRoot || (resolved.config ? resolved.projectRoot : null),
108
+ },
109
+ projectConfig,
110
+ vaultBase: resolved.base,
111
+ };
112
+ }
113
+
114
+ export function setSessionOperatingProfile(vaultBase, sessionId, profile, { now } = {}) {
115
+ const selected = normalizeOperatingProfile(profile, { strict: true });
116
+ const updatedAt = now || new Date().toISOString();
117
+ return mutateSessionRegistry(vaultBase, (registry) => {
118
+ const sessions = registry.sessions || (registry.sessions = {});
119
+ if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
120
+ const current = sessions[sessionId];
121
+ sessions[sessionId] = {
122
+ ...current,
123
+ operating_profile: selected,
124
+ operating_profile_source: 'explicit-cli',
125
+ operating_profile_updated_at: updatedAt,
126
+ updated_at: updatedAt,
127
+ };
128
+ return sessions[sessionId];
129
+ });
130
+ }
131
+
132
+ function sessionProfile(vaultBase, sessionId, projectResolved) {
133
+ const sessions = readSessionRegistry(vaultBase).sessions || {};
134
+ if (!Object.hasOwn(sessions, sessionId)) throw new Error(`sessão não encontrada: ${sessionId}`);
135
+ const entry = sessions[sessionId];
136
+ if (Object.hasOwn(entry, 'operating_profile')) {
137
+ try {
138
+ return {
139
+ profile: normalizeOperatingProfile(entry.operating_profile, { strict: true }),
140
+ source: 'session-registry',
141
+ };
142
+ } catch {
143
+ return {
144
+ profile: DEFAULT_OPERATING_PROFILE,
145
+ source: 'session-override-invalid',
146
+ };
147
+ }
148
+ }
149
+ return { profile: projectResolved.profile, source: projectResolved.source };
150
+ }
151
+
152
+ export function runProfile(argv = []) {
153
+ try { validateArgv(argv); }
154
+ catch (error) { return fail(error.message); }
155
+ const args = commandArgs(argv);
156
+ const sub = args[0] || 'status';
157
+ const json = argv.includes('--json');
158
+ const sessionId = optionValue(argv, '--session') || '';
159
+
160
+ let state;
161
+ try { state = context(argv); }
162
+ catch (error) { return fail(error.message); }
163
+
164
+ const projectResolved = resolveOperatingProfile(state.projectConfig);
165
+ if (sub === 'status' || sub === 'show') {
166
+ if (args.length > 1) return fail(`${sub} não aceita argumentos posicionais adicionais`);
167
+ try {
168
+ const effective = sessionId
169
+ ? sessionProfile(state.vaultBase, sessionId, projectResolved)
170
+ : { profile: projectResolved.profile, source: projectResolved.source };
171
+ output({
172
+ profile: effective.profile,
173
+ source: effective.source,
174
+ scope: sessionId ? 'session' : 'project',
175
+ session_id: sessionId || null,
176
+ ...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
177
+ }, json);
178
+ return 0;
179
+ } catch (error) { return fail(error.message); }
180
+ }
181
+
182
+ if (sub !== 'use' && sub !== 'set') return fail('use status | use <OFF|FLOW|GUIDE|GOVERN|ASSURE>');
183
+ if (args.length !== 2) return fail(`${sub} requer exatamente um perfil`);
184
+ let profile;
185
+ try { profile = normalizeOperatingProfile(args[1], { strict: true }); }
186
+ catch { return fail('perfil inválido; use OFF, FLOW, GUIDE, GOVERN ou ASSURE'); }
187
+
188
+ if (sessionId) {
189
+ try { setSessionOperatingProfile(state.vaultBase, sessionId, profile); }
190
+ catch (error) { return fail(error.message); }
191
+ output({
192
+ profile,
193
+ source: 'session-registry',
194
+ scope: 'session',
195
+ session_id: sessionId,
196
+ ...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
197
+ }, json);
198
+ return 0;
199
+ }
200
+
201
+ if (!state.resolved.projectRoot) return fail('binding de projeto necessário para alterar o perfil padrão');
202
+ try {
203
+ const updatedAt = new Date().toISOString();
204
+ updateProjectBinding(state.resolved.projectRoot, (current) => {
205
+ const next = setOperatingProfile(current, profile);
206
+ return {
207
+ ...next,
208
+ harness: {
209
+ ...next.harness,
210
+ profileSource: 'explicit-cli',
211
+ profileUpdatedAt: updatedAt,
212
+ },
213
+ };
214
+ });
215
+ } catch (error) { return fail(error.message); }
216
+ output({
217
+ profile,
218
+ source: 'project-binding',
219
+ scope: 'project',
220
+ session_id: null,
221
+ ...(state.resolved.bindingError ? { binding_error: state.resolved.bindingError } : {}),
222
+ }, json);
223
+ return 0;
224
+ }
@@ -13,6 +13,16 @@ export const PROJECT_CONFIG_FILE = '.wendkeep.json';
13
13
  export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
14
14
  export const PROJECT_CONFIG_SCHEMA = 1;
15
15
 
16
+ const PROJECT_VAULT_INTEGRITY_CODES = new Set([
17
+ 'WENDKEEP_VAULT_CONFIG_INVALID',
18
+ 'WENDKEEP_VAULT_MARKER_MISSING',
19
+ 'WENDKEEP_VAULT_PROJECT_MISMATCH',
20
+ ]);
21
+
22
+ export function isProjectVaultIntegrityError(error) {
23
+ return PROJECT_VAULT_INTEGRITY_CODES.has(error?.code);
24
+ }
25
+
16
26
  function json(path) {
17
27
  try { return JSON.parse(readFileSync(path, 'utf8')); }
18
28
  catch (error) {
@@ -62,8 +72,23 @@ function inputStart(input = {}, fallback = '') {
62
72
  || process.cwd();
63
73
  }
64
74
 
75
+ function bindingDiagnostic(error) {
76
+ return {
77
+ code: error?.code || 'WENDKEEP_VAULT_CONFIG_INVALID',
78
+ message: error?.message || 'Configuração WendKeep inválida.',
79
+ };
80
+ }
81
+
65
82
  function vaultFromConfig(projectRoot, config) {
66
- if (!config || config.schemaVersion !== PROJECT_CONFIG_SCHEMA || !config.projectId || !config.vault) {
83
+ const valid = config
84
+ && typeof config === 'object'
85
+ && !Array.isArray(config)
86
+ && config.schemaVersion === PROJECT_CONFIG_SCHEMA
87
+ && typeof config.projectId === 'string'
88
+ && config.projectId.trim()
89
+ && typeof config.vault === 'string'
90
+ && config.vault.trim();
91
+ if (!valid) {
67
92
  const error = new Error(
68
93
  `Configuração incompleta em "${join(projectRoot, PROJECT_CONFIG_FILE)}". `
69
94
  + 'Rode `wendkeep init --project <path> --vault <path>`.',
@@ -104,9 +129,14 @@ export function findLegacyProjectVault(start) {
104
129
  source: 'legacy-project-settings',
105
130
  configPath: settingsPath,
106
131
  projectId: '',
132
+ config: null,
107
133
  };
108
134
  }
109
- } catch { /* init/doctor explicam JSON inválido; descoberta segue procurando */ }
135
+ } catch (error) {
136
+ const wrapped = new Error(`Configuração WendKeep legada inválida em "${settingsPath}": ${error.message}`);
137
+ wrapped.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
138
+ throw wrapped;
139
+ }
110
140
  }
111
141
  return null;
112
142
  }
@@ -147,16 +177,30 @@ export function resolveProjectVault({
147
177
  const start = inputStart(input, startDir);
148
178
  const explicit = explicitVault || input?.obsidian_vault_path;
149
179
  if (explicit) {
180
+ let bindingError = null;
181
+ try { findProjectBinding(start); }
182
+ catch (error) {
183
+ if (error?.code !== 'WENDKEEP_VAULT_CONFIG_INVALID') throw error;
184
+ bindingError = bindingDiagnostic(error);
185
+ }
150
186
  return {
151
187
  base: isAbsolute(explicit) ? resolve(explicit) : resolve(startDirectory(start), explicit),
152
188
  source: explicitVault ? 'explicit' : 'payload',
153
189
  projectRoot: startDirectory(start),
154
190
  projectId: '',
155
191
  configPath: '',
192
+ config: null,
193
+ ...(bindingError ? { bindingError } : {}),
156
194
  };
157
195
  }
158
196
 
159
- const binding = findProjectBinding(start);
197
+ let binding = null;
198
+ let bindingFailure = null;
199
+ try { binding = findProjectBinding(start); }
200
+ catch (error) {
201
+ if (error?.code !== 'WENDKEEP_VAULT_CONFIG_INVALID') throw error;
202
+ bindingFailure = error;
203
+ }
160
204
  if (binding) {
161
205
  const result = {
162
206
  base: binding.base,
@@ -164,6 +208,7 @@ export function resolveProjectVault({
164
208
  projectRoot: binding.projectRoot,
165
209
  projectId: binding.config.projectId,
166
210
  configPath: binding.configPath,
211
+ config: binding.config,
167
212
  };
168
213
  if (validateIdentity) validateMarker(result);
169
214
  return result;
@@ -171,9 +216,16 @@ export function resolveProjectVault({
171
216
 
172
217
  if (allowLegacySettings) {
173
218
  const legacy = findLegacyProjectVault(start);
174
- if (legacy) return legacy;
219
+ if (legacy) {
220
+ return {
221
+ ...legacy,
222
+ ...(bindingFailure ? { bindingError: bindingDiagnostic(bindingFailure) } : {}),
223
+ };
224
+ }
175
225
  }
176
226
 
227
+ if (bindingFailure) throw bindingFailure;
228
+
177
229
  const error = new Error(
178
230
  `Nenhum vault WendKeep vinculado ao projeto em "${startDirectory(start)}". `
179
231
  + `Crie ${PROJECT_CONFIG_FILE} com \`wendkeep init --project "${startDirectory(start)}" --vault <path> --yes\`.`,
@@ -188,7 +240,11 @@ function portableVaultPath(projectRoot, vaultPath) {
188
240
  return vaultPath;
189
241
  }
190
242
 
191
- export function bindProjectVault({ projectRoot, vaultPath }) {
243
+ function objectRecord(value) {
244
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
245
+ }
246
+
247
+ export function bindProjectVault({ projectRoot, vaultPath, configPatch = {} }) {
192
248
  const root = resolve(projectRoot);
193
249
  const base = isAbsolute(vaultPath) ? resolve(vaultPath) : resolve(root, vaultPath);
194
250
  const existing = readProjectBinding(root);
@@ -205,12 +261,23 @@ export function bindProjectVault({ projectRoot, vaultPath }) {
205
261
  }
206
262
 
207
263
  mkdirSync(join(base, '.brain'), { recursive: true });
264
+ const previousConfig = objectRecord(existing?.config);
265
+ const patch = objectRecord(configPatch);
208
266
  const config = {
267
+ ...previousConfig,
268
+ ...patch,
209
269
  schemaVersion: PROJECT_CONFIG_SCHEMA,
210
270
  projectId,
211
271
  vault: portableVaultPath(root, base),
212
272
  };
273
+ if (previousConfig.harness || patch.harness) {
274
+ config.harness = {
275
+ ...objectRecord(previousConfig.harness),
276
+ ...objectRecord(patch.harness),
277
+ };
278
+ }
213
279
  const marker = {
280
+ ...objectRecord(existingMarker?.marker),
214
281
  schemaVersion: PROJECT_CONFIG_SCHEMA,
215
282
  projectId,
216
283
  projectName: basename(root),
@@ -219,3 +286,41 @@ export function bindProjectVault({ projectRoot, vaultPath }) {
219
286
  atomicJson(join(root, PROJECT_CONFIG_FILE), config);
220
287
  return { base, projectRoot: root, projectId, config, marker };
221
288
  }
289
+
290
+ export function updateProjectBinding(projectRoot, updater) {
291
+ const binding = readProjectBinding(projectRoot);
292
+ if (!binding) {
293
+ const error = new Error(
294
+ `Nenhum binding WendKeep em "${resolve(projectRoot)}". Rode \`wendkeep init\` primeiro.`,
295
+ );
296
+ error.code = 'WENDKEEP_VAULT_UNCONFIGURED';
297
+ throw error;
298
+ }
299
+ if (typeof updater !== 'function') {
300
+ throw new TypeError('updateProjectBinding exige uma função updater.');
301
+ }
302
+
303
+ const current = {
304
+ ...binding.config,
305
+ ...(binding.config.harness ? { harness: { ...objectRecord(binding.config.harness) } } : {}),
306
+ };
307
+ const candidate = updater(current);
308
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
309
+ const error = new Error('Updater do binding WendKeep deve retornar um objeto de configuração.');
310
+ error.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
311
+ throw error;
312
+ }
313
+
314
+ const config = {
315
+ ...candidate,
316
+ schemaVersion: PROJECT_CONFIG_SCHEMA,
317
+ projectId: binding.config.projectId,
318
+ vault: binding.config.vault,
319
+ };
320
+ atomicJson(binding.configPath, config);
321
+ return {
322
+ ...binding,
323
+ config,
324
+ base: vaultFromConfig(binding.projectRoot, config),
325
+ };
326
+ }
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
6
6
  import { updateSessionObservability } from '../hooks/session-observability.mjs';
7
+ import { assertVaultPathSafe } from '../hooks/vault-path-safety.mjs';
7
8
 
8
9
  export function rebuildSessionCosts(vaultBase, { apply = false, session = '', limit = 0 } = {}) {
9
10
  const registry = readSessionRegistry(vaultBase);
@@ -14,14 +15,20 @@ export function rebuildSessionCosts(vaultBase, { apply = false, session = '', li
14
15
  for (const entry of entries) {
15
16
  if (limit && report.scanned >= limit) break;
16
17
  report.scanned += 1;
17
- const note = join(vaultBase, entry.session_file);
18
- if (!entry.transcript_path || !existsSync(note) || !existsSync(entry.transcript_path)) {
19
- report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: existsSync(note), transcript: !!entry.transcript_path && existsSync(entry.transcript_path), transcriptPath: entry.transcript_path || '' });
18
+ const checkedNote = assertVaultPathSafe(vaultBase, join(vaultBase, entry.session_file), {
19
+ expectedType: 'file', label: 'nota de sessão do rebuild de custos',
20
+ });
21
+ const note = checkedNote.target;
22
+ if (!entry.transcript_path || !checkedNote.exists || !existsSync(entry.transcript_path)) {
23
+ report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: checkedNote.exists, transcript: !!entry.transcript_path && existsSync(entry.transcript_path), transcriptPath: entry.transcript_path || '' });
20
24
  continue;
21
25
  }
22
26
  const before = readFileSync(note, 'utf8');
23
27
  try {
24
- updateSessionObservability({ sessionPath: note, transcriptPath: entry.transcript_path, caller: 'cost-rebuild', canonicalConversationId: entry.sessionId });
28
+ updateSessionObservability({
29
+ vaultBase, sessionPath: note, transcriptPath: entry.transcript_path,
30
+ caller: 'cost-rebuild', canonicalConversationId: entry.sessionId,
31
+ });
25
32
  const after = readFileSync(note, 'utf8');
26
33
  const changed = before !== after;
27
34
  if (changed) report.changed += 1; else report.unchanged += 1;
@@ -11,18 +11,29 @@ function skill(name, description, body, files = []) {
11
11
  return { name, description, body: `---\nname: ${name}\ndescription: ${description}\n---\n${body}`, files };
12
12
  }
13
13
 
14
- const WORKFLOW = `# Loop a2o ciclo de trabalho do wendkeep
14
+ const WORKFLOW = `# Perfis de Operação roteador de trabalho do wendkeep
15
15
 
16
- Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault) e prova
17
- (sensores) juntas, tudo linkado no grafo do Obsidian.
16
+ Use ao começar implementação, correção ou refatoração. **Keep Core permanece sempre ativo**
17
+ em todos os perfis: Vault, sessão, identidade, memória, lessons e persistência. Na ausência de
18
+ configuração válida, **GOVERN é o padrão** compatível.
18
19
 
19
20
  <HARD-GATE>
20
- NÃO edite arquivos de código antes do passo 2 (Propose / \`wendkeep change new\`).
21
- Toda tarefa não-trivial passa pelo loop planejar no chat e sair editando deixa o
22
- vault cego. Exceção única: mudança trivial (typo, 1 linha).
21
+ Antes de editar, leia o **perfil efetivo** injetado pelo WendKeep e siga somente sua rota:
22
+ - \`OFF\`: não imponha processo Wend; a governança pertence ao **harness nativo da LLM**.
23
+ - \`FLOW\`: inicie o microcontrato com \`wendkeep flow start\` antes de editar os paths permitidos.
24
+ - \`GUIDE\`, \`GOVERN\` ou \`ASSURE\`: não edite código antes de Propose / \`wendkeep change new\`.
25
+ Este gate nunca transforma \`OFF\` ou \`FLOW\` silenciosamente em \`GOVERN\`.
23
26
  </HARD-GATE>
24
27
 
25
- ## Os passos
28
+ ## Rotas por perfil
29
+
30
+ - **OFF — LLM nativa:** Wend Runtime desligado; esta skill devolve a execução ao harness nativo.
31
+ - **FLOW — E → V:** \`flow start\` → implementar com wk-tdd → \`flow finish\`; sem change/ADR/verdict.
32
+ - **GUIDE — P → E → V:** change compacta, sem revisão formal obrigatória.
33
+ - **GOVERN — P → R → E → V:** loop a2 atual, com design/revisão; é o padrão conservador.
34
+ - **ASSURE — P → R → E → V → C:** GOVERN acrescido de confirmação e handoff explícitos.
35
+
36
+ ## Passos para GUIDE, GOVERN e ASSURE
26
37
 
27
38
  1. **Explore** — entenda o problema antes de propor. Leia o código/contexto relevante.
28
39
  2. **Propose** — \`wendkeep change new <slug>\`. Isso cria \`08-Mudanças/<slug>/\` com:
@@ -251,18 +262,29 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
251
262
  - \`verdict-template.json\` — o formato exato do \`verdict.json\` a gravar.
252
263
  `;
253
264
 
254
- const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
265
+ const WORKFLOW_EN = `# Operating Profiles — wendkeep work router
255
266
 
256
- Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
257
- (sensors) together, wikilinked in the Obsidian graph.
267
+ Use this when starting an implementation, fix, or refactor. **Keep Core is always active**
268
+ in every profile: Vault, session, identity, memory, lessons, and persistence. With no valid
269
+ configuration, **GOVERN is the default** for compatibility.
258
270
 
259
271
  <HARD-GATE>
260
- Do NOT edit code files before step 2 (Propose / \`wendkeep change new\`).
261
- Every non-trivial task goes through the loop planning in chat and editing right away
262
- leaves the vault blind. Single exception: a trivial change (typo, one line).
272
+ Before editing, read the injected **effective profile** and follow only its route:
273
+ - \`OFF\`: impose no Wend process; governance belongs to the **native LLM harness**.
274
+ - \`FLOW\`: start the microcontract with \`wendkeep flow start\` before editing allowed paths.
275
+ - \`GUIDE\`, \`GOVERN\`, or \`ASSURE\`: do not edit code before Propose / \`wendkeep change new\`.
276
+ This gate never silently turns \`OFF\` or \`FLOW\` into \`GOVERN\`.
263
277
  </HARD-GATE>
264
278
 
265
- ## Steps
279
+ ## Profile routes
280
+
281
+ - **OFF — native LLM:** Wend Runtime is disabled; this skill returns execution to the native harness.
282
+ - **FLOW — E → V:** \`flow start\` → implement with wk-tdd → \`flow finish\`; no change/ADR/verdict.
283
+ - **GUIDE — P → E → V:** a compact change with no mandatory formal review.
284
+ - **GOVERN — P → R → E → V:** the current a2 loop with design/review; the conservative default.
285
+ - **ASSURE — P → R → E → V → C:** GOVERN plus explicit confirmation and handoff.
286
+
287
+ ## Steps for GUIDE, GOVERN, and ASSURE
266
288
 
267
289
  1. **Explore** — understand the problem before proposing.
268
290
  2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
@@ -597,7 +619,7 @@ size of the problem>
597
619
  // usuário ("implementa X", "corrige Y"), não com abstrações ("mudança não-trivial"). Gatilhos
598
620
  // concretos + instrução imperativa = a skill dispara sozinha (paridade Superpowers).
599
621
  const WK_SKILLS_PT = [
600
- skill('wk-workflow', 'Use SEMPRE que o usuário pedir para implementar, criar, corrigir, refatorar, adicionar ou alterar código qualquer tarefa de código não-trivial. Invoque ANTES de editar qualquer arquivo: orquestra o loop a2 (wendkeep change new → tarefas → verify → archive) e registra tudo no vault.', WORKFLOW),
622
+ skill('wk-workflow', 'Use quando o usuário pedir para implementar, criar, corrigir, refatorar, adicionar ou alterar código: leia o perfil efetivo e roteie OFF/FLOW/GUIDE/GOVERN/ASSURE ANTES de editar. Keep Core permanece ativo; GOVERN é o padrão compatível.', WORKFLOW),
601
623
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
602
624
  skill('wk-debugging', 'Use quando algo falha, quebra, dá erro ou regride — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
603
625
  skill('wk-brainstorming', 'Use quando a ideia ainda é vaga ou o usuário quer discutir/planejar uma feature (inclusive em plan mode) — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
@@ -606,7 +628,7 @@ const WK_SKILLS_PT = [
606
628
  ];
607
629
 
608
630
  const WK_SKILLS_EN = [
609
- skill('wk-workflow', 'Use WHENEVER the user asks to implement, create, fix, refactor, add or change code — any non-trivial coding task. Invoke BEFORE editing any file: it orchestrates the a2 loop (wendkeep change new tasks verify archive) and records everything in the vault.', WORKFLOW_EN),
631
+ skill('wk-workflow', 'Use when the user asks to implement, create, fix, refactor, add, or change code: read the effective profile and route OFF/FLOW/GUIDE/GOVERN/ASSURE BEFORE editing. Keep Core stays active; GOVERN is the compatible default.', WORKFLOW_EN),
610
632
  skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
611
633
  skill('wk-debugging', 'Use when something fails, breaks, errors or regresses — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
612
634
  skill('wk-brainstorming', 'Use when the idea is still vague or the user wants to discuss/plan a feature (plan mode included) — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),