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.
Files changed (45) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.en.md +37 -16
  3. package/README.md +37 -16
  4. package/docs/en/commands/changes-and-verification.md +10 -5
  5. package/docs/en/commands/maintenance-and-diagnostics.md +17 -9
  6. package/docs/en/commands/memory.md +16 -1
  7. package/docs/en/commands/observer.md +8 -1
  8. package/docs/en/commands/operating-profiles.md +28 -3
  9. package/docs/pt-BR/commands/changes-and-verification.md +10 -5
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -5
  11. package/docs/pt-BR/commands/memory.md +16 -1
  12. package/docs/pt-BR/commands/observer.md +7 -1
  13. package/docs/pt-BR/commands/operating-profiles.md +28 -3
  14. package/hooks/brain-core.mjs +2 -0
  15. package/hooks/brain-inject.mjs +6 -6
  16. package/hooks/brain-recall.mjs +5 -1
  17. package/hooks/change-context.mjs +11 -0
  18. package/hooks/change-core.mjs +53 -21
  19. package/hooks/change-warn.mjs +2 -0
  20. package/hooks/evidence-context.mjs +41 -0
  21. package/hooks/evidence-recall.mjs +1 -0
  22. package/hooks/harness-doctor.mjs +13 -5
  23. package/hooks/memory-scope.mjs +1 -0
  24. package/hooks/vault-health.mjs +2 -2
  25. package/package.json +2 -2
  26. package/packages/cli/src/index.mjs +13 -3
  27. package/packages/integrations/src/host-hooks.mjs +1 -0
  28. package/packages/vault/src/evidence-recall.mjs +343 -0
  29. package/packages/vault/src/index.mjs +2 -0
  30. package/packages/vault/src/memory-handoff.mjs +58 -3
  31. package/packages/vault/src/memory-schema.mjs +12 -2
  32. package/packages/vault/src/memory-scope.mjs +119 -0
  33. package/packages/vault/src/memory-store.mjs +86 -24
  34. package/schema/observer/004-evidence-recall.sql +25 -0
  35. package/src/change.mjs +10 -4
  36. package/src/delivery.mjs +303 -0
  37. package/src/doctor.mjs +47 -10
  38. package/src/memory.mjs +95 -2
  39. package/src/observer-sql-store.mjs +141 -5
  40. package/src/release-provenance.mjs +47 -0
  41. package/src/skills-seed.mjs +25 -9
  42. package/src/sync-defs.mjs +5 -2
  43. package/src/sync.mjs +2 -2
  44. package/src/taxonomy.mjs +4 -0
  45. package/src/work-kind.mjs +62 -0
@@ -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
- warnings.push(...defs.issues.map((issue) => `defs: ${issue}`));
108
- warnings.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
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[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\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
- process.stdout.write(`\n${renderUnpricedModelLines(checkUnpricedModels(vaultBase)).join('\n')}\n`);
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
- process.stdout.write(`\n${renderStaleDerivedSectionLines(checkStaleDerivedSections(vaultBase)).join('\n')}\n`);
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
- process.stdout.write(`\n${renderSessionObservabilityLines(checkSessionObservability(vaultBase)).join('\n')}\n`);
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
- return healthStatus !== 0 || errors.length ? 1 : 0;
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;