wendkeep 0.72.1 → 0.74.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 +61 -0
- package/README.en.md +37 -16
- package/README.md +37 -16
- package/docs/en/commands/changes-and-verification.md +10 -5
- package/docs/en/commands/maintenance-and-diagnostics.md +17 -9
- package/docs/en/commands/memory.md +16 -1
- package/docs/en/commands/observer.md +8 -1
- package/docs/en/commands/operating-profiles.md +28 -3
- package/docs/pt-BR/commands/changes-and-verification.md +10 -5
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -5
- package/docs/pt-BR/commands/memory.md +16 -1
- package/docs/pt-BR/commands/observer.md +7 -1
- package/docs/pt-BR/commands/operating-profiles.md +28 -3
- package/hooks/brain-core.mjs +2 -0
- package/hooks/brain-inject.mjs +6 -6
- package/hooks/brain-recall.mjs +5 -1
- package/hooks/change-context.mjs +11 -0
- package/hooks/change-core.mjs +53 -21
- package/hooks/change-warn.mjs +2 -0
- package/hooks/evidence-context.mjs +41 -0
- package/hooks/evidence-recall.mjs +1 -0
- package/hooks/harness-doctor.mjs +13 -5
- package/hooks/memory-scope.mjs +1 -0
- package/hooks/vault-health.mjs +2 -2
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +13 -3
- package/packages/integrations/src/host-hooks.mjs +1 -0
- package/packages/vault/src/evidence-recall.mjs +343 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/memory-handoff.mjs +58 -3
- package/packages/vault/src/memory-schema.mjs +12 -2
- package/packages/vault/src/memory-scope.mjs +119 -0
- package/packages/vault/src/memory-store.mjs +86 -24
- package/schema/observer/004-evidence-recall.sql +25 -0
- package/src/change.mjs +10 -4
- package/src/delivery.mjs +303 -0
- package/src/doctor.mjs +47 -10
- package/src/memory.mjs +95 -2
- package/src/observer-sql-store.mjs +141 -5
- package/src/release-provenance.mjs +47 -0
- package/src/skills-seed.mjs +25 -9
- package/src/sync-defs.mjs +5 -2
- package/src/sync.mjs +2 -2
- package/src/taxonomy.mjs +4 -0
- package/src/work-kind.mjs +62 -0
package/src/delivery.mjs
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
|
|
6
|
+
} from '../hooks/vault-path-safety.mjs';
|
|
7
|
+
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
|
+
import { createWorkRoute } from './work-kind.mjs';
|
|
9
|
+
|
|
10
|
+
export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
11
|
+
|
|
12
|
+
start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
13
|
+
status [id]
|
|
14
|
+
finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>]
|
|
15
|
+
[--npm-integrity <sha512-...>] [--release-url <url>]
|
|
16
|
+
abandon [id] --reason <text>
|
|
17
|
+
|
|
18
|
+
Common options: --project <path> --vault <path> --json
|
|
19
|
+
Delivery authorizes operational risk and creates an append-only receipt. It never creates a change,
|
|
20
|
+
spec, or ADR. If code/config must change, abandon or pause delivery and resume an implementation.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export const DELIVERY_CAPABILITIES = Object.freeze([
|
|
24
|
+
'git:merge', 'git:pull', 'git:push', 'git:tag', 'publish',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const VALUE_OPTIONS = new Set([
|
|
28
|
+
'--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
|
|
29
|
+
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function parseArgv(argv) {
|
|
33
|
+
const values = new Map();
|
|
34
|
+
const positionals = [];
|
|
35
|
+
let json = false;
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const item = argv[index];
|
|
38
|
+
if (item === '--json') { json = true; continue; }
|
|
39
|
+
if (item.startsWith('--')) {
|
|
40
|
+
const eq = item.indexOf('=');
|
|
41
|
+
const name = eq > 0 ? item.slice(0, eq) : item;
|
|
42
|
+
if (!VALUE_OPTIONS.has(name)) throw new Error(`opção desconhecida: ${name}`);
|
|
43
|
+
const value = eq > 0 ? item.slice(eq + 1) : argv[++index];
|
|
44
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} requer um valor`);
|
|
45
|
+
const list = values.get(name) || [];
|
|
46
|
+
list.push(value);
|
|
47
|
+
values.set(name, list);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
positionals.push(item);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
json,
|
|
54
|
+
positionals,
|
|
55
|
+
value: (name) => values.get(name)?.at(-1) || '',
|
|
56
|
+
all: (name) => values.get(name) || [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function git(projectRoot, args, optional = false) {
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync('git', args, {
|
|
63
|
+
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
+
}).trim();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (optional) return '';
|
|
67
|
+
throw new Error(`git ${args.join(' ')} falhou: ${String(error.stderr || error.message).trim()}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deliveryPaths(vaultBase, id = '') {
|
|
72
|
+
const runtime = join(vaultBase, '.brain', 'runtime');
|
|
73
|
+
const deliveries = join(runtime, 'deliveries');
|
|
74
|
+
return {
|
|
75
|
+
runtime,
|
|
76
|
+
deliveries,
|
|
77
|
+
pointer: join(runtime, 'CURRENT_DELIVERY'),
|
|
78
|
+
receipts: join(runtime, 'delivery-receipts.jsonl'),
|
|
79
|
+
state: id ? join(deliveries, `${id}.json`) : '',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeId(value) {
|
|
84
|
+
const id = String(value || '').trim();
|
|
85
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,100}$/i.test(id)) throw new Error('id de delivery inválido');
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function generatedId(now = new Date()) {
|
|
90
|
+
return `delivery-${now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z').toLowerCase()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readPointer(vaultBase) {
|
|
94
|
+
const { pointer } = deliveryPaths(vaultBase);
|
|
95
|
+
try { return safeId(readFileSync(pointer, 'utf8').trim()); } catch { return ''; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function activeDelivery(vaultBase) {
|
|
99
|
+
const id = readPointer(vaultBase);
|
|
100
|
+
if (!id) return null;
|
|
101
|
+
try {
|
|
102
|
+
const state = readState(vaultBase, id);
|
|
103
|
+
return state.state === 'active' ? state : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readState(vaultBase, id) {
|
|
110
|
+
const path = deliveryPaths(vaultBase, id).state;
|
|
111
|
+
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
112
|
+
allowMissing: false, expectedType: 'file', label: `delivery ${id}`,
|
|
113
|
+
});
|
|
114
|
+
return JSON.parse(readFileSync(checked.target, 'utf8'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeState(vaultBase, state) {
|
|
118
|
+
const paths = deliveryPaths(vaultBase, state.id);
|
|
119
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
120
|
+
mkdirVaultPath(vaultBase, paths.deliveries, { label: 'estados de delivery' });
|
|
121
|
+
writeVaultFileSync(vaultBase, paths.state, `${JSON.stringify(state, null, 2)}\n`, 'utf8', {
|
|
122
|
+
label: `delivery ${state.id}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setPointer(vaultBase, id = '') {
|
|
127
|
+
const paths = deliveryPaths(vaultBase);
|
|
128
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
129
|
+
writeVaultFileSync(vaultBase, paths.pointer, id ? `${id}\n` : '', 'utf8', {
|
|
130
|
+
label: 'ponteiro de delivery',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function appendReceipt(vaultBase, receipt) {
|
|
135
|
+
const paths = deliveryPaths(vaultBase);
|
|
136
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
137
|
+
let previous = '';
|
|
138
|
+
if (existsSync(paths.receipts)) {
|
|
139
|
+
const checked = assertVaultPathSafe(vaultBase, paths.receipts, {
|
|
140
|
+
allowMissing: false, expectedType: 'file', label: 'ledger de receipts de delivery',
|
|
141
|
+
});
|
|
142
|
+
previous = readFileSync(checked.target, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
writeVaultFileSync(vaultBase, paths.receipts, `${previous}${JSON.stringify(receipt)}\n`, 'utf8', {
|
|
145
|
+
label: 'ledger de receipts de delivery',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function context(parsed) {
|
|
150
|
+
const projectRoot = resolve(parsed.value('--project') || process.cwd());
|
|
151
|
+
const resolved = resolveProjectVault({
|
|
152
|
+
startDir: projectRoot,
|
|
153
|
+
explicitVault: parsed.value('--vault'),
|
|
154
|
+
});
|
|
155
|
+
const repoRoot = git(projectRoot, ['rev-parse', '--show-toplevel']);
|
|
156
|
+
return { projectRoot, repoRoot, vaultBase: resolved.base };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function currentId(parsed, vaultBase) {
|
|
160
|
+
return safeId(parsed.positionals[1] || readPointer(vaultBase));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureClean(repoRoot) {
|
|
164
|
+
const dirty = git(repoRoot, ['status', '--porcelain']);
|
|
165
|
+
if (dirty) {
|
|
166
|
+
const error = new Error('delivery requer working tree limpa; alterações de código/config exigem implementation.');
|
|
167
|
+
error.code = 'WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED';
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function startDelivery({ vaultBase, repoRoot, id, capabilities, sourceChange = '', sourceCommit = '', now = new Date() }) {
|
|
173
|
+
ensureClean(repoRoot);
|
|
174
|
+
const deliveryId = safeId(id || generatedId(now));
|
|
175
|
+
const paths = deliveryPaths(vaultBase, deliveryId);
|
|
176
|
+
if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
|
|
177
|
+
const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
|
|
178
|
+
git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
179
|
+
const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
|
|
180
|
+
const invalidCapabilities = requestedCapabilities.filter((item) => !DELIVERY_CAPABILITIES.includes(item));
|
|
181
|
+
if (invalidCapabilities.length) {
|
|
182
|
+
throw new Error(`capability inválida: ${invalidCapabilities.join(', ')}. Use ${DELIVERY_CAPABILITIES.join(', ')}.`);
|
|
183
|
+
}
|
|
184
|
+
const route = createWorkRoute({
|
|
185
|
+
workKind: 'delivery', profile: 'ASSURE', contractImpact: 'none',
|
|
186
|
+
operationRisk: requestedCapabilities, sourceChange, sourceCommit: commit,
|
|
187
|
+
});
|
|
188
|
+
if (!route.operation_risk.length) throw new Error('delivery start requer ao menos um --allow <capability>');
|
|
189
|
+
const state = {
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
id: deliveryId,
|
|
192
|
+
state: 'active',
|
|
193
|
+
route,
|
|
194
|
+
repository: repoRoot,
|
|
195
|
+
worktree: repoRoot,
|
|
196
|
+
branch: git(repoRoot, ['branch', '--show-current'], true),
|
|
197
|
+
source_commit: commit,
|
|
198
|
+
started_at: now.toISOString(),
|
|
199
|
+
};
|
|
200
|
+
writeState(vaultBase, state);
|
|
201
|
+
setPointer(vaultBase, deliveryId);
|
|
202
|
+
return state;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function finishDelivery({ vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, now = new Date() }) {
|
|
206
|
+
ensureClean(repoRoot);
|
|
207
|
+
const state = readState(vaultBase, safeId(id));
|
|
208
|
+
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
209
|
+
const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
|
|
210
|
+
git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
|
|
211
|
+
const capabilities = state.route?.operation_risk || [];
|
|
212
|
+
if (capabilities.includes('git:tag') || capabilities.includes('publish')) {
|
|
213
|
+
if (!evidence.version) throw new Error('delivery com tag/publicação requer --version');
|
|
214
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
|
215
|
+
if (pkg.version !== evidence.version) throw new Error(`package.json ${pkg.version} diverge de ${evidence.version}`);
|
|
216
|
+
const tagCommit = git(repoRoot, ['rev-list', '-n', '1', `refs/tags/v${evidence.version}`]);
|
|
217
|
+
if (tagCommit !== targetCommit) throw new Error(`v${evidence.version} não aponta para o target comprovado`);
|
|
218
|
+
}
|
|
219
|
+
if (capabilities.includes('publish')) {
|
|
220
|
+
for (const [key, label] of [
|
|
221
|
+
['ci_url', '--ci-url'], ['npm_integrity', '--npm-integrity'], ['release_url', '--release-url'],
|
|
222
|
+
]) if (!evidence[key]) throw new Error(`delivery com publish requer ${label}`);
|
|
223
|
+
}
|
|
224
|
+
const receipt = {
|
|
225
|
+
schema_version: 1,
|
|
226
|
+
delivery_id: state.id,
|
|
227
|
+
outcome: 'completed',
|
|
228
|
+
work_kind: 'delivery',
|
|
229
|
+
source_change: state.route.source_change || '',
|
|
230
|
+
source_commit: state.source_commit,
|
|
231
|
+
target,
|
|
232
|
+
target_commit: targetCommit,
|
|
233
|
+
capabilities,
|
|
234
|
+
evidence,
|
|
235
|
+
finished_at: now.toISOString(),
|
|
236
|
+
};
|
|
237
|
+
appendReceipt(vaultBase, receipt);
|
|
238
|
+
writeState(vaultBase, { ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt });
|
|
239
|
+
if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
240
|
+
return receipt;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function abandonDelivery({ vaultBase, id, reason, now = new Date() }) {
|
|
244
|
+
const state = readState(vaultBase, safeId(id));
|
|
245
|
+
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
246
|
+
if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
|
|
247
|
+
const receipt = {
|
|
248
|
+
schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
|
|
249
|
+
reason: String(reason).trim(), abandoned_at: now.toISOString(),
|
|
250
|
+
};
|
|
251
|
+
appendReceipt(vaultBase, receipt);
|
|
252
|
+
writeState(vaultBase, { ...state, state: 'abandoned', reason: receipt.reason, abandoned_at: receipt.abandoned_at });
|
|
253
|
+
if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
254
|
+
return receipt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function emit(payload, json) {
|
|
258
|
+
if (json) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
259
|
+
else process.stdout.write(`${payload.id || payload.delivery_id}: ${payload.state || payload.outcome}\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function runDelivery(argv = []) {
|
|
263
|
+
try {
|
|
264
|
+
const parsed = parseArgv(argv);
|
|
265
|
+
const sub = parsed.positionals[0] || 'status';
|
|
266
|
+
const { repoRoot, vaultBase } = context(parsed);
|
|
267
|
+
if (sub === 'start') {
|
|
268
|
+
const state = startDelivery({
|
|
269
|
+
vaultBase, repoRoot, id: parsed.positionals[1], capabilities: parsed.all('--allow'),
|
|
270
|
+
sourceChange: parsed.value('--source-change'), sourceCommit: parsed.value('--source-commit'),
|
|
271
|
+
});
|
|
272
|
+
emit(state, parsed.json);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
if (sub === 'status') {
|
|
276
|
+
const state = readState(vaultBase, currentId(parsed, vaultBase));
|
|
277
|
+
emit(state, parsed.json);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
if (sub === 'finish') {
|
|
281
|
+
const receipt = finishDelivery({
|
|
282
|
+
vaultBase, repoRoot, id: currentId(parsed, vaultBase), target: parsed.value('--target') || 'HEAD',
|
|
283
|
+
evidence: {
|
|
284
|
+
ci_url: parsed.value('--ci-url'), version: parsed.value('--version'),
|
|
285
|
+
npm_integrity: parsed.value('--npm-integrity'), release_url: parsed.value('--release-url'),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
emit(receipt, parsed.json);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
if (sub === 'abandon') {
|
|
292
|
+
const receipt = abandonDelivery({
|
|
293
|
+
vaultBase, id: currentId(parsed, vaultBase), reason: parsed.value('--reason'),
|
|
294
|
+
});
|
|
295
|
+
emit(receipt, parsed.json);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`subcomando desconhecido: ${sub}`);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
process.stderr.write(`wendkeep delivery: ${error.message}\n`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { checkSyncDefs } from './sync-defs.mjs';
|
|
|
7
7
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
8
|
|
|
9
9
|
const healthStatusLabel = (status) => ({
|
|
10
|
-
healthy: 'saudável', warning: 'atenção', blocked: 'bloqueada', legacy: 'legado',
|
|
10
|
+
healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
|
|
11
11
|
}[status] || status || 'desconhecido');
|
|
12
12
|
|
|
13
13
|
const metricValue = (value) => value === null || value === undefined || value === '' ? 'n/a' : value;
|
|
@@ -55,6 +55,8 @@ export function runDoctor(argv) {
|
|
|
55
55
|
let vault;
|
|
56
56
|
let project;
|
|
57
57
|
let session = '';
|
|
58
|
+
let scope = 'all';
|
|
59
|
+
let strict = false;
|
|
58
60
|
for (let i = 0; i < argv.length; i += 1) {
|
|
59
61
|
const a = argv[i];
|
|
60
62
|
if (a === '--vault') vault = argv[++i];
|
|
@@ -63,6 +65,13 @@ export function runDoctor(argv) {
|
|
|
63
65
|
else if (a.startsWith('--project=')) project = a.slice(10);
|
|
64
66
|
else if (a === '--session') session = argv[++i] || '';
|
|
65
67
|
else if (a.startsWith('--session=')) session = a.slice(10);
|
|
68
|
+
else if (a === '--scope') scope = argv[++i] || '';
|
|
69
|
+
else if (a.startsWith('--scope=')) scope = a.slice(8);
|
|
70
|
+
else if (a === '--strict') strict = true;
|
|
71
|
+
}
|
|
72
|
+
if (!['all', 'core', 'runtime'].includes(scope)) {
|
|
73
|
+
process.stderr.write('wendkeep doctor: --scope deve ser all, core ou runtime\n');
|
|
74
|
+
return 2;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
const projectRoot = resolve(project || process.cwd());
|
|
@@ -97,18 +106,29 @@ export function runDoctor(argv) {
|
|
|
97
106
|
memoryStatus: 'blocked',
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
|
-
process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
|
|
109
|
+
if (scope !== 'runtime') process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
|
|
101
110
|
const healthStatus = health.ok ? 0 : 1;
|
|
102
111
|
|
|
112
|
+
if (scope === 'core') {
|
|
113
|
+
const strictDebt = strict && (
|
|
114
|
+
(health.warnings || []).length > 0
|
|
115
|
+
|| !['healthy'].includes(health.memoryStatus)
|
|
116
|
+
);
|
|
117
|
+
process.stdout.write(`\n[core] ${healthStatus ? 'erro estrutural' : health.memoryStatus === 'degraded' ? 'saudável com memória degradada' : 'saudável'}\n`);
|
|
118
|
+
return healthStatus || strictDebt ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
// 2. Harness integrity (Wave B).
|
|
104
|
-
const { errors, warnings } = checkHarness(vaultBase, projectRoot);
|
|
122
|
+
const { errors, warnings, attention, repairable } = checkHarness(vaultBase, projectRoot);
|
|
105
123
|
const defs = checkSyncDefs(vaultBase, projectRoot);
|
|
106
124
|
if (!defs.ok) {
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
repairable.push(...defs.issues.map((issue) => `defs: ${issue}`));
|
|
126
|
+
repairable.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
|
|
109
127
|
}
|
|
110
|
-
process.stdout.write(`\n[
|
|
128
|
+
process.stdout.write(`\n[runtime] ${errors.length} erro(s) estrutural(is), ${attention.length} atenção(ões), ${repairable.length} reparável(is), ${warnings.length} aviso(s)\n`);
|
|
111
129
|
for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
|
|
130
|
+
for (const item of attention) process.stdout.write(` ! ${item}\n`);
|
|
131
|
+
for (const item of repairable) process.stdout.write(` → ${item}\n`);
|
|
112
132
|
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
113
133
|
|
|
114
134
|
// 3. Link/graph health — órfãos que o grafo do Obsidian mostraria, com o comando de reparo.
|
|
@@ -125,13 +145,16 @@ export function runDoctor(argv) {
|
|
|
125
145
|
process.stdout.write(`\n${renderStackedFrontmatterLines(vaultBase, stacked).join('\n')}\n`);
|
|
126
146
|
|
|
127
147
|
// 3c. Modelo fora de pricing.json fecha a sessão com custo zero, sem erro — só aparece aqui.
|
|
128
|
-
|
|
148
|
+
const unpriced = checkUnpricedModels(vaultBase);
|
|
149
|
+
process.stdout.write(`\n${renderUnpricedModelLines(unpriced).join('\n')}\n`);
|
|
129
150
|
|
|
130
151
|
// 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
|
|
131
|
-
|
|
152
|
+
const staleDerived = checkStaleDerivedSections(vaultBase);
|
|
153
|
+
process.stdout.write(`\n${renderStaleDerivedSectionLines(staleDerived).join('\n')}\n`);
|
|
132
154
|
|
|
133
155
|
// 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
|
|
134
|
-
|
|
156
|
+
const observability = checkSessionObservability(vaultBase);
|
|
157
|
+
process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
|
|
135
158
|
|
|
136
159
|
// 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
|
|
137
160
|
const act = checkSessionActivity(vaultBase);
|
|
@@ -146,5 +169,19 @@ export function runDoctor(argv) {
|
|
|
146
169
|
|
|
147
170
|
// Devolve o código em vez de sair: `wendkeep sync` encadeia este comando, e um
|
|
148
171
|
// process.exit aqui mataria a cadeia. Quem faz o exit é o bin.
|
|
149
|
-
|
|
172
|
+
const strictDebt = strict && (
|
|
173
|
+
(scope !== 'runtime' && (health.warnings || []).length)
|
|
174
|
+
|| (scope !== 'runtime' && health.memoryStatus !== 'healthy')
|
|
175
|
+
|| attention.length
|
|
176
|
+
|| repairable.length
|
|
177
|
+
|| warnings.length
|
|
178
|
+
|| links.derivedOrphans
|
|
179
|
+
|| links.artifactOrphans
|
|
180
|
+
|| links.graphColors === false
|
|
181
|
+
|| stacked.count
|
|
182
|
+
|| (unpriced.models || unpriced.items || []).length
|
|
183
|
+
|| (staleDerived.notes || staleDerived.items || []).length
|
|
184
|
+
|| !observability.ok
|
|
185
|
+
);
|
|
186
|
+
return (scope !== 'runtime' && healthStatus !== 0) || errors.length || strictDebt ? 1 : 0;
|
|
150
187
|
}
|
package/src/memory.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './validate-memory.mjs';
|
|
21
21
|
import { validateCore } from './validate-core.mjs';
|
|
22
22
|
import { checkMemoryBundle } from '../hooks/vault-health.mjs';
|
|
23
|
+
import { scopeForMemoryKey } from '../hooks/memory-scope.mjs';
|
|
23
24
|
|
|
24
25
|
const BRAIN = '.brain';
|
|
25
26
|
const LEDGER = 'MEMORY_EVENTS.jsonl';
|
|
@@ -229,6 +230,92 @@ export function migrateMemory(vault, {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
function rescopeEventId(sourceEventId, scope) {
|
|
234
|
+
return `mem-rescope-${hash(`${sourceEventId}\0${scope.type}\0${scope.id}`).slice(0, 24)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Plan an append-only migration without choosing a winner for ambiguous keys. */
|
|
238
|
+
export function planScopedMemoryMigration(vault) {
|
|
239
|
+
const ledger = readMemoryLedger(vault);
|
|
240
|
+
if (ledger.status !== 'ok') throw new Error('Ledger corrompido; execute memory repair antes do rescope.');
|
|
241
|
+
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
242
|
+
const ambiguous = new Set(projection.candidates.map((candidate) => candidate.record_key || candidate.memory_key));
|
|
243
|
+
const existingIds = new Set(ledger.events.map((event) => event.event_id));
|
|
244
|
+
const planned = [];
|
|
245
|
+
|
|
246
|
+
for (const [recordKey, record] of Object.entries(projection.records)) {
|
|
247
|
+
if (ambiguous.has(recordKey) || record.source?.scope) continue;
|
|
248
|
+
const source = record.source;
|
|
249
|
+
const sameLegacyKey = ledger.events.filter((event) => (
|
|
250
|
+
!event.scope && event.memory_key === source.memory_key && !event.rescopes_event_id
|
|
251
|
+
));
|
|
252
|
+
const scope = scopeForMemoryKey(source.memory_key, {
|
|
253
|
+
...source,
|
|
254
|
+
projectId: source.project_id,
|
|
255
|
+
workSessionId: source.work_session_id,
|
|
256
|
+
branch: source.value?.branch || source.value?.branch_name,
|
|
257
|
+
worktreeId: source.value?.worktree_id,
|
|
258
|
+
repositoryId: source.value?.repository_id,
|
|
259
|
+
});
|
|
260
|
+
const eventId = rescopeEventId(source.event_id, scope);
|
|
261
|
+
if (existingIds.has(eventId)) continue;
|
|
262
|
+
planned.push({
|
|
263
|
+
v: 1,
|
|
264
|
+
event_id: eventId,
|
|
265
|
+
project_id: source.project_id,
|
|
266
|
+
memory_key: source.memory_key,
|
|
267
|
+
scope,
|
|
268
|
+
operation: 'assert',
|
|
269
|
+
value: record.value,
|
|
270
|
+
authority: source.authority,
|
|
271
|
+
canonical_session_id: source.canonical_session_id || 'memory-rescope',
|
|
272
|
+
activation_id: source.activation_id || 'memory-rescope',
|
|
273
|
+
activation_epoch: Number.isInteger(source.activation_epoch) ? source.activation_epoch : 0,
|
|
274
|
+
turn_sequence: Number.isInteger(source.turn_sequence) ? source.turn_sequence : 0,
|
|
275
|
+
source_turn_id: source.source_turn_id || 'memory-rescope',
|
|
276
|
+
observed_at: source.observed_at || new Date(0).toISOString(),
|
|
277
|
+
evidence: [...new Set([...(source.evidence || []), `memory-rescope:${source.event_id}`])],
|
|
278
|
+
rescopes_event_id: source.event_id,
|
|
279
|
+
rescopes_event_ids: sameLegacyKey.map((event) => event.event_id).sort(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
status: 'dry-run',
|
|
285
|
+
ledger_events: ledger.events.length,
|
|
286
|
+
planned: planned.length,
|
|
287
|
+
ambiguous: projection.candidates.length,
|
|
288
|
+
events: planned,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function rescopeMemoryEvents(vault, { apply = false } = {}) {
|
|
293
|
+
projectId(vault);
|
|
294
|
+
const plan = planScopedMemoryMigration(vault);
|
|
295
|
+
if (!apply) {
|
|
296
|
+
return {
|
|
297
|
+
status: 'dry-run',
|
|
298
|
+
ledger_events: plan.ledger_events,
|
|
299
|
+
planned: plan.planned,
|
|
300
|
+
ambiguous: plan.ambiguous,
|
|
301
|
+
scopes: plan.events.map((event) => ({
|
|
302
|
+
event_id: event.event_id, memory_key: event.memory_key, scope: event.scope,
|
|
303
|
+
})),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (!plan.events.length) {
|
|
307
|
+
return { status: 'unchanged', migrated: 0, ambiguous: plan.ambiguous };
|
|
308
|
+
}
|
|
309
|
+
for (const event of plan.events) enqueueMemoryEvent(vault, event);
|
|
310
|
+
const projection = projectMemoryOutbox(vault);
|
|
311
|
+
return {
|
|
312
|
+
status: projection.status === 'projected' ? 'migrated' : projection.status,
|
|
313
|
+
migrated: plan.events.length,
|
|
314
|
+
ambiguous: plan.ambiguous,
|
|
315
|
+
checkpoint: projection.checkpoint || null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
232
319
|
function readCandidates(vault) {
|
|
233
320
|
const path = brainPath(vault, CANDIDATES);
|
|
234
321
|
const checked = checkedVaultFile(vault, path, 'MEMORY_CANDIDATES.jsonl');
|
|
@@ -266,6 +353,7 @@ function sanitizedCandidate(candidate, index) {
|
|
|
266
353
|
reason: candidate.reason,
|
|
267
354
|
status: candidate.status || 'active',
|
|
268
355
|
memory_key: candidate.memory_key,
|
|
356
|
+
...(candidate.scope ? { scope: candidate.scope } : {}),
|
|
269
357
|
event_ids: [...eventIds].sort(lexicalCompare),
|
|
270
358
|
};
|
|
271
359
|
}
|
|
@@ -328,6 +416,7 @@ export function listMemoryCandidatesForCuration(vault) {
|
|
|
328
416
|
reason: safe.reason,
|
|
329
417
|
status: safe.status,
|
|
330
418
|
memory_key: safe.memory_key,
|
|
419
|
+
...(safe.scope ? { scope: safe.scope } : {}),
|
|
331
420
|
events: safe.event_ids.map((eventId) => sanitizedCurationEvent(candidate, eventId, index)),
|
|
332
421
|
};
|
|
333
422
|
})
|
|
@@ -388,7 +477,7 @@ function promotedSupersedes(vault, candidate, selected) {
|
|
|
388
477
|
const ledger = readMemoryLedger(vault);
|
|
389
478
|
if (ledger.status !== 'ok') throw new Error('Ledger de memória inválido durante a promoção.');
|
|
390
479
|
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
391
|
-
const current = projection.records?.[candidate.memory_key]?.source;
|
|
480
|
+
const current = projection.records?.[candidate.record_key || candidate.memory_key]?.source;
|
|
392
481
|
if (!current?.event_id) return [...memberIds].sort();
|
|
393
482
|
if (memberIds.has(current.event_id)) {
|
|
394
483
|
projection.superseded
|
|
@@ -572,6 +661,9 @@ export function decideMemoryCandidate(vault, {
|
|
|
572
661
|
event_id: `cli-${action}-${hash(`${candidateId}\0${selected?.event_id || ''}`).slice(0, 20)}`,
|
|
573
662
|
project_id: projectId(vault),
|
|
574
663
|
memory_key: action === 'promote' ? candidate.memory_key : `candidate.decision.${candidateId}`,
|
|
664
|
+
scope: action === 'promote'
|
|
665
|
+
? (candidate.scope || selected?.scope || { type: 'project', id: projectId(vault) })
|
|
666
|
+
: { type: 'project', id: projectId(vault) },
|
|
575
667
|
operation: action === 'promote' && selected ? 'replace' : 'assert',
|
|
576
668
|
value: action === 'promote' ? promotedMemoryValue(selectedValue) : 'rejected',
|
|
577
669
|
authority: 'verified',
|
|
@@ -2087,6 +2179,7 @@ export function runMemory(argv) {
|
|
|
2087
2179
|
result = listMemoryCandidates(vault, { activeOnly: candidatesArgs.activeOnly });
|
|
2088
2180
|
}
|
|
2089
2181
|
else if (sub === 'migrate') result = migrateMemory(vault, { apply: argv.includes('--apply') });
|
|
2182
|
+
else if (sub === 'rescope') result = rescopeMemoryEvents(vault, { apply: argv.includes('--apply') });
|
|
2090
2183
|
else if (sub === 'repair') result = repairMemory(vault);
|
|
2091
2184
|
else if (sub === 'reconcile') {
|
|
2092
2185
|
result = reconcileMemory(vault, {
|
|
@@ -2109,7 +2202,7 @@ export function runMemory(argv) {
|
|
|
2109
2202
|
action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
|
|
2110
2203
|
});
|
|
2111
2204
|
}
|
|
2112
|
-
else { process.stderr.write('wendkeep memory: use status | candidates [--active] | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
2205
|
+
else { process.stderr.write('wendkeep memory: use status | candidates [--active] | curate | migrate [--apply] | rescope [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
2113
2206
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2114
2207
|
if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
|
|
2115
2208
|
else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;
|