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.
- package/CHANGELOG.md +93 -0
- package/README.en.md +45 -3
- package/README.md +45 -3
- package/bin/wendkeep.mjs +54 -6
- package/docs/en/commands/changes-and-verification.md +9 -3
- package/docs/en/commands/getting-started.md +7 -3
- package/docs/en/commands/memory.md +20 -2
- package/docs/en/commands/operating-profiles.md +173 -0
- package/docs/en/commands/sessions-and-import.md +8 -4
- package/docs/en/commands/verify.md +12 -6
- package/docs/pt-BR/commands/changes-and-verification.md +9 -4
- package/docs/pt-BR/commands/getting-started.md +7 -3
- package/docs/pt-BR/commands/memory.md +18 -2
- package/docs/pt-BR/commands/operating-profiles.md +171 -0
- package/docs/pt-BR/commands/sessions-and-import.md +7 -3
- package/docs/pt-BR/commands/verify.md +11 -5
- 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 +39 -55
- 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 +6 -4
- package/hooks/session-iteration.mjs +65 -0
- package/hooks/session-memory-lifecycle.mjs +10 -5
- package/hooks/session-note-io.mjs +130 -15
- package/hooks/session-observability.mjs +4 -2
- package/hooks/session-stop.mjs +65 -19
- 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 +183 -37
- package/hooks/vault-path-safety.mjs +2 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +10 -3
- package/packages/cli/package.json +5 -0
- package/packages/harness/package.json +5 -0
- package/packages/integrations/package.json +5 -0
- package/packages/mcp/package.json +5 -0
- package/packages/pi/package.json +5 -0
- package/packages/vault/package.json +6 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/project-vault.mjs +327 -0
- package/packages/vault/src/vault-path-safety.mjs +558 -0
- 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 +2 -221
- 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 +8 -0
- package/src/validate-memory.mjs +21 -8
- 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
|
-
|
|
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(
|
|
625
|
+
log(M.runtimeIgnore);
|
|
603
626
|
|
|
604
627
|
// 4. Vault color system (.obsidian) -----------------------------------------
|
|
605
628
|
if (args.noColors) {
|