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.
- package/CHANGELOG.md +73 -0
- package/README.en.md +41 -3
- package/README.md +41 -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 +558 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +3 -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 +8 -0
- package/src/validate-memory.mjs +21 -8
- 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
|
+
}
|
package/src/profile.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/project-vault.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
+
}
|
package/src/rebuild-costs.mjs
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
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({
|
|
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;
|
package/src/skills-seed.mjs
CHANGED
|
@@ -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 = `#
|
|
14
|
+
const WORKFLOW = `# Perfis de Operação — roteador de trabalho do wendkeep
|
|
15
15
|
|
|
16
|
-
Use ao começar
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
##
|
|
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 = `#
|
|
265
|
+
const WORKFLOW_EN = `# Operating Profiles — wendkeep work router
|
|
255
266
|
|
|
256
|
-
Use
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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 }]),
|