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
package/src/flow.mjs ADDED
@@ -0,0 +1,232 @@
1
+ // Public FLOW CLI. The durable contract lives in the Vault runtime store; this
2
+ // module only parses the canonical command surface and renders deterministic exits.
3
+ import {
4
+ finishFlow, flowStatus, promoteFlow, startFlow,
5
+ } from '../hooks/flow-core.mjs';
6
+ import { resolveProjectVault } from './project-vault.mjs';
7
+
8
+ export const FLOW_HELP = `wendkeep flow <subcommand>
9
+
10
+ start <slug> --allow <path>... --sensor <id>... --reason <text> [--session <id>]
11
+ status [<id>]
12
+ show <id>
13
+ finish <id>
14
+ promote <id> [--change-slug <slug>]
15
+
16
+ Common options: --project <path> --vault <path> --session <id> --json
17
+ FLOW has no --force option.
18
+ `;
19
+
20
+ function usage(message) {
21
+ const error = new Error(message);
22
+ error.code = 'FLOW_USAGE';
23
+ return error;
24
+ }
25
+
26
+ const VALUE_OPTIONS = new Map([
27
+ ['--allow', 'allow'],
28
+ ['--sensor', 'sensor'],
29
+ ['--reason', 'reason'],
30
+ ['--session', 'session'],
31
+ ['--project', 'project'],
32
+ ['--vault', 'vault'],
33
+ ['--change-slug', 'changeSlug'],
34
+ ]);
35
+
36
+ function parseOptions(argv) {
37
+ const values = {
38
+ allow: [], sensor: [], reason: '', session: '', project: '', vault: '', changeSlug: '', json: false,
39
+ };
40
+ const positionals = [];
41
+ const seenSingletons = new Set();
42
+ const setOption = (name, key, value) => {
43
+ if (key === 'allow' || key === 'sensor') {
44
+ values[key].push(String(value));
45
+ return;
46
+ }
47
+ if (seenSingletons.has(name)) throw usage(`opção duplicada: ${name}`);
48
+ seenSingletons.add(name);
49
+ values[key] = String(value);
50
+ };
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const arg = String(argv[index]);
53
+ if (arg === '--json') {
54
+ if (seenSingletons.has(arg)) throw usage(`opção duplicada: ${arg}`);
55
+ seenSingletons.add(arg);
56
+ values.json = true;
57
+ continue;
58
+ }
59
+ if (VALUE_OPTIONS.has(arg)) {
60
+ const value = argv[++index];
61
+ if (value === undefined || String(value).startsWith('--')) throw usage(`valor ausente para ${arg}`);
62
+ const key = VALUE_OPTIONS.get(arg);
63
+ setOption(arg, key, value);
64
+ continue;
65
+ }
66
+ if (arg.startsWith('--')) {
67
+ const at = arg.indexOf('=');
68
+ const name = at === -1 ? arg : arg.slice(0, at);
69
+ if (!VALUE_OPTIONS.has(name)) throw usage(`opção desconhecida: ${name}`);
70
+ const value = arg.slice(at + 1);
71
+ if (!value) throw usage(`valor ausente para ${name}`);
72
+ const key = VALUE_OPTIONS.get(name);
73
+ setOption(name, key, value);
74
+ continue;
75
+ }
76
+ positionals.push(arg);
77
+ }
78
+ return { ...values, positionals };
79
+ }
80
+
81
+ function context(options, io) {
82
+ const startDir = options.project || io.cwd || process.cwd();
83
+ if (options.vault) {
84
+ return resolveProjectVault({ startDir, explicitVault: options.vault });
85
+ }
86
+ try {
87
+ return resolveProjectVault({ startDir });
88
+ } catch (bindingError) {
89
+ const fallback = io.env?.OBSIDIAN_VAULT_PATH;
90
+ if (!fallback) throw bindingError;
91
+ return resolveProjectVault({ startDir, explicitVault: fallback });
92
+ }
93
+ }
94
+
95
+ function writeJson(stream, value) {
96
+ stream.write(`${JSON.stringify(value)}\n`);
97
+ }
98
+
99
+ function writeState(stream, state, { detailed = false } = {}) {
100
+ const { contract } = state;
101
+ stream.write(`FLOW ${contract.flow_id}: ${state.state} (${contract.slug})\n`);
102
+ stream.write(`session: ${contract.session_id}\n`);
103
+ stream.write(`reason: ${contract.reason}\n`);
104
+ stream.write(`allow: ${contract.allowed_paths.join(', ')}\n`);
105
+ stream.write(`sensors: ${contract.sensor_ids.join(', ')}\n`);
106
+ if (detailed || state.attempts.length) stream.write(`attempts: ${state.attempts.length}\n`);
107
+ if (state.receipt) stream.write(`changed: ${state.receipt.changed_paths.join(', ')}\n`);
108
+ if (state.promotion) stream.write(`change: ${state.promotion.change_slug}\n`);
109
+ }
110
+
111
+ function fail(stream, error, json) {
112
+ const message = error?.message || String(error);
113
+ if (json) writeJson(stream, { ok: false, error: message, code: error?.code || 'FLOW_ERROR' });
114
+ else stream.write(`wendkeep flow: ${message}\n`);
115
+ return 2;
116
+ }
117
+
118
+ function requirePositionals(positionals, count, command) {
119
+ if (positionals.length !== count) {
120
+ const expected = command === 'status' ? '[<id>]' : command === 'start' ? '<slug>' : '<id>';
121
+ throw usage(`use: wendkeep flow ${command} ${expected}`);
122
+ }
123
+ }
124
+
125
+ function rejectStartOnlyOptions(options, command) {
126
+ if (options.allow.length || options.sensor.length || options.reason) {
127
+ throw usage(`--allow, --sensor e --reason só são válidos em flow start (recebido: ${command})`);
128
+ }
129
+ }
130
+
131
+ export async function runFlow(argv = [], streams = {}) {
132
+ const io = {
133
+ stdout: streams.stdout || process.stdout,
134
+ stderr: streams.stderr || process.stderr,
135
+ env: streams.env ?? process.env,
136
+ cwd: streams.cwd || process.cwd(),
137
+ };
138
+ if (argv.includes('--help') || argv.includes('-h')) {
139
+ io.stdout.write(FLOW_HELP);
140
+ return 0;
141
+ }
142
+
143
+ let options;
144
+ let sub;
145
+ try {
146
+ [sub = 'status'] = argv;
147
+ options = parseOptions(argv.slice(sub ? 1 : 0));
148
+ if (!['start', 'status', 'show', 'finish', 'promote'].includes(sub)) {
149
+ throw usage('use start | status | show | finish | promote');
150
+ }
151
+ } catch (error) {
152
+ return fail(io.stderr, error, argv.includes('--json'));
153
+ }
154
+
155
+ let resolved;
156
+ try {
157
+ if (options.changeSlug && sub !== 'promote') {
158
+ throw usage(`--change-slug só é válido em flow promote (recebido: ${sub})`);
159
+ }
160
+ if (sub === 'start') {
161
+ requirePositionals(options.positionals, 1, sub);
162
+ if (!options.reason) throw usage('--reason é obrigatório em flow start');
163
+ if (!options.allow.length) throw usage('ao menos um --allow é obrigatório em flow start');
164
+ if (!options.sensor.length) throw usage('ao menos um --sensor é obrigatório em flow start');
165
+ } else {
166
+ rejectStartOnlyOptions(options, sub);
167
+ requirePositionals(options.positionals, sub === 'status' ? (options.positionals.length ? 1 : 0) : 1, sub);
168
+ }
169
+ resolved = context(options, io);
170
+
171
+ if (sub === 'start') {
172
+ const state = startFlow({
173
+ vaultBase: resolved.base,
174
+ projectRoot: resolved.projectRoot || io.cwd,
175
+ projectId: resolved.projectId || resolved.config?.projectId || '',
176
+ slug: options.positionals[0],
177
+ allowedPaths: options.allow,
178
+ sensorIds: options.sensor,
179
+ reason: options.reason,
180
+ sessionId: options.session,
181
+ env: io.env,
182
+ });
183
+ if (options.json) writeJson(io.stdout, state);
184
+ else writeState(io.stdout, state, { detailed: true });
185
+ return 0;
186
+ }
187
+
188
+ if (sub === 'status' || sub === 'show') {
189
+ const state = flowStatus(resolved.base, {
190
+ flowId: options.positionals[0] || '',
191
+ sessionId: options.session,
192
+ env: io.env,
193
+ });
194
+ if (options.json) writeJson(io.stdout, state);
195
+ else writeState(io.stdout, state, { detailed: sub === 'show' });
196
+ return 0;
197
+ }
198
+
199
+ if (sub === 'finish') {
200
+ const result = finishFlow({
201
+ vaultBase: resolved.base,
202
+ projectRoot: resolved.projectRoot || io.cwd,
203
+ flowId: options.positionals[0],
204
+ sessionId: options.session,
205
+ });
206
+ if (options.json) writeJson(io.stdout, result);
207
+ else if (result.ok) writeState(io.stdout, result.state, { detailed: true });
208
+ else {
209
+ io.stdout.write(`FLOW ${options.positionals[0]} bloqueado:\n`);
210
+ for (const failure of result.failures) io.stdout.write(`- ${failure}\n`);
211
+ }
212
+ return result.ok ? 0 : 1;
213
+ }
214
+
215
+ const result = promoteFlow({
216
+ vaultBase: resolved.base,
217
+ projectRoot: resolved.projectRoot || io.cwd,
218
+ flowId: options.positionals[0],
219
+ sessionId: options.session,
220
+ changeSlug: options.changeSlug,
221
+ });
222
+ if (options.json) writeJson(io.stdout, result);
223
+ else if (result.ok) writeState(io.stdout, result.state, { detailed: true });
224
+ else {
225
+ io.stdout.write(`FLOW ${options.positionals[0]} com promoção pendente:\n`);
226
+ for (const failure of result.failures) io.stdout.write(`- ${failure}\n`);
227
+ }
228
+ return result.ok ? 0 : 1;
229
+ } catch (error) {
230
+ return fail(io.stderr, error, options?.json);
231
+ }
232
+ }
package/src/init.mjs CHANGED
@@ -40,15 +40,28 @@ import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSe
40
40
  import { adoptSpecsState, ensureSpecsReadme, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
41
41
  import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
42
42
  import { seedMemoryV2 } from './memory.mjs';
43
+ import {
44
+ DEFAULT_OPERATING_PROFILE,
45
+ normalizeOperatingProfile,
46
+ setOperatingProfile,
47
+ } from './operating-profile.mjs';
43
48
 
44
49
  function parseArgs(argv) {
45
- const args = { mcp: true, yes: false, force: false };
50
+ const args = { mcp: true, yes: false, force: false, profileProvided: false };
46
51
  for (let i = 0; i < argv.length; i += 1) {
47
52
  const a = argv[i];
48
53
  if (a === '--vault') args.vault = argv[++i];
49
54
  else if (a === '--locale') args.locale = argv[++i];
50
55
  else if (a.startsWith('--locale=')) args.locale = a.slice(9);
51
56
  else if (a === '--project') args.project = argv[++i];
57
+ else if (a === '--profile') {
58
+ args.profileProvided = true;
59
+ args.profile = argv[++i];
60
+ }
61
+ else if (a.startsWith('--profile=')) {
62
+ args.profileProvided = true;
63
+ args.profile = a.slice(10);
64
+ }
52
65
  else if (a === '--no-mcp') args.mcp = false;
53
66
  else if (a === '--yes' || a === '-y') args.yes = true;
54
67
  else if (a === '--force') args.force = true;
@@ -308,6 +321,7 @@ const MESSAGES = {
308
321
  mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
309
322
  colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
310
323
  colors: (r) => ` [5/5] cores: ${r}`,
324
+ runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/',
311
325
  merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
312
326
  nextSteps: '\nPróximos passos:',
313
327
  step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
@@ -336,6 +350,7 @@ const MESSAGES = {
336
350
  mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
337
351
  colorsSkipped: ' [5/5] colors skipped (--no-colors)',
338
352
  colors: (r) => ` [5/5] colors: ${r}`,
353
+ runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/',
339
354
  merged: 'merged', created: 'created', bakSaved: ', .bak saved',
340
355
  nextSteps: '\nNext steps:',
341
356
  step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
@@ -385,6 +400,11 @@ export async function runInit(argv) {
385
400
  const args = parseArgs(argv);
386
401
  const projectPath = resolve(args.project || process.cwd());
387
402
  const log = (s) => process.stdout.write(`${s}\n`);
403
+ const requestedProfile = args.profileProvided
404
+ ? normalizeOperatingProfile(args.profile, { strict: true })
405
+ : undefined;
406
+ let existingBinding = null;
407
+ try { existingBinding = readProjectBinding(projectPath); } catch { /* bind surfaces invalid config */ }
388
408
 
389
409
  // Recognize an already-configured project through `.wendkeep.json`, or through the
390
410
  // project-local Claude setting during migration. Locale remains locked in vault config.
@@ -424,7 +444,10 @@ export async function runInit(argv) {
424
444
 
425
445
  // Persist the provider-neutral binding before any hook/config delivery. This also
426
446
  // claims the vault identity and rejects accidental cross-project graph contamination.
427
- bindProjectVault({ projectRoot: projectPath, vaultPath });
447
+ const profile = requestedProfile
448
+ ?? (existingBinding ? undefined : DEFAULT_OPERATING_PROFILE);
449
+ const configPatch = profile === undefined ? {} : setOperatingProfile({}, profile);
450
+ bindProjectVault({ projectRoot: projectPath, vaultPath, configPatch });
428
451
 
429
452
  // Companion plugins/MCP selection. --no-companions wins; --companions <csv> is
430
453
  // explicit; an interactive TTY gets a multi-choice prompt (context-mode pre-checked);
@@ -599,7 +622,7 @@ export async function runInit(argv) {
599
622
  log(M.mcpSkipped);
600
623
  }
601
624
 
602
- log(' [!] ignore runtime do wendkeep no Git quando o vault for versionado: .brain/.change-*');
625
+ log(M.runtimeIgnore);
603
626
 
604
627
  // 4. Vault color system (.obsidian) -----------------------------------------
605
628
  if (args.noColors) {