wendkeep 0.58.1 → 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 +120 -0
- package/README.en.md +70 -40
- package/README.md +70 -40
- package/bin/wendkeep.mjs +54 -6
- package/docs/en/commands/changes-and-verification.md +85 -0
- package/docs/en/commands/costs-and-observability.md +65 -0
- package/docs/en/commands/getting-started.md +86 -0
- package/docs/en/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/en/commands/memory-migration.md +73 -0
- package/docs/en/commands/memory.md +102 -0
- package/docs/en/commands/notes-and-knowledge.md +70 -0
- package/docs/en/commands/operating-profiles.md +173 -0
- package/docs/en/commands/retroactive-import.md +67 -0
- package/docs/en/commands/sessions-and-import.md +89 -0
- package/docs/en/commands/verify.md +92 -0
- package/docs/pt-BR/commands/changes-and-verification.md +85 -0
- package/docs/pt-BR/commands/costs-and-observability.md +65 -0
- package/docs/pt-BR/commands/getting-started.md +87 -0
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +77 -0
- package/docs/pt-BR/commands/memory-migration.md +73 -0
- package/docs/pt-BR/commands/memory.md +99 -0
- package/docs/pt-BR/commands/notes-and-knowledge.md +69 -0
- package/docs/pt-BR/commands/operating-profiles.md +171 -0
- package/docs/pt-BR/commands/retroactive-import.md +67 -0
- package/docs/pt-BR/commands/sessions-and-import.md +89 -0
- package/docs/pt-BR/commands/verify.md +93 -0
- 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 +119 -84
- 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 +21 -12
- package/hooks/session-iteration.mjs +65 -0
- package/hooks/session-memory-lifecycle.mjs +335 -0
- package/hooks/session-note-io.mjs +130 -15
- package/hooks/session-observability.mjs +4 -2
- package/hooks/session-stop.mjs +181 -59
- 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 +268 -25
- package/hooks/vault-path-safety.mjs +558 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +5 -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 +9 -0
- package/src/validate-memory.mjs +21 -8
- package/src/verify.mjs +12 -2
package/hooks/spec-core.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// hooks/spec-core.mjs — living spec (07-Specs) + change delta merge (OpenSpec native).
|
|
2
2
|
// Pure parsing/merge + promoteSpecs (fs). No import from change-core (avoids a cycle).
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { existsSync, readFileSync, readdirSync
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { ensureDir } from './obsidian-common.mjs';
|
|
7
6
|
import { getLocale } from './locale.mjs';
|
|
7
|
+
import {
|
|
8
|
+
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileSync,
|
|
9
|
+
} from './vault-path-safety.mjs';
|
|
8
10
|
|
|
9
11
|
// Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
|
|
10
12
|
export function tasksHashOf(md) {
|
|
@@ -122,8 +124,14 @@ export function livingSpecCapabilities(vaultBase) {
|
|
|
122
124
|
|
|
123
125
|
export function adoptSpecsState(vaultBase) {
|
|
124
126
|
const state = { version: 1, generatedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz do estado de specs' });
|
|
128
|
+
writeVaultFileSync(
|
|
129
|
+
vaultBase,
|
|
130
|
+
join(vaultBase, SPECS_STATE_FILE),
|
|
131
|
+
`${JSON.stringify(state, null, 2)}\n`,
|
|
132
|
+
'utf8',
|
|
133
|
+
{ label: 'estado consolidado de specs' },
|
|
134
|
+
);
|
|
127
135
|
return state;
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -150,17 +158,32 @@ function recordPromotedSpecs(vaultBase, capabilities) {
|
|
|
150
158
|
else delete specs[capability];
|
|
151
159
|
}
|
|
152
160
|
const state = { version: 1, generatedAt: new Date().toISOString(), specs };
|
|
153
|
-
|
|
161
|
+
writeVaultFileSync(
|
|
162
|
+
vaultBase,
|
|
163
|
+
join(vaultBase, SPECS_STATE_FILE),
|
|
164
|
+
`${JSON.stringify(state, null, 2)}\n`,
|
|
165
|
+
'utf8',
|
|
166
|
+
{ label: 'estado consolidado de specs' },
|
|
167
|
+
);
|
|
154
168
|
return state;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } = {}) {
|
|
158
172
|
const path = join(changeDir, SPEC_BASELINE_FILE);
|
|
159
|
-
|
|
173
|
+
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
174
|
+
expectedType: 'file', label: 'baseline de specs da change',
|
|
175
|
+
});
|
|
176
|
+
if (!refresh && checked.exists) {
|
|
160
177
|
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
|
|
161
178
|
}
|
|
162
179
|
const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
|
|
163
|
-
|
|
180
|
+
writeVaultFileSync(
|
|
181
|
+
vaultBase,
|
|
182
|
+
path,
|
|
183
|
+
`${JSON.stringify(baseline, null, 2)}\n`,
|
|
184
|
+
'utf8',
|
|
185
|
+
{ label: 'baseline de specs da change' },
|
|
186
|
+
);
|
|
164
187
|
return baseline;
|
|
165
188
|
}
|
|
166
189
|
|
|
@@ -325,7 +348,7 @@ export function ensureSpecsReadme(vaultBase) {
|
|
|
325
348
|
const loc = getLocale(vaultBase);
|
|
326
349
|
const en = loc.id === 'en';
|
|
327
350
|
const dir = join(vaultBase, loc.folders.specs);
|
|
328
|
-
|
|
351
|
+
mkdirVaultPath(vaultBase, dir, { label: 'raiz de specs consolidadas' });
|
|
329
352
|
const body = en
|
|
330
353
|
? `# Specs — generated living contract
|
|
331
354
|
|
|
@@ -355,7 +378,48 @@ Pense como código-fonte vs commits: esta pasta é o *código atual* de cada cap
|
|
|
355
378
|
\`wendkeep change archive\` promove para esta pasta.
|
|
356
379
|
- Histórico por mudança → \`${loc.folders.changes}/_arquivo/\`. Contrato atual → aqui.
|
|
357
380
|
`;
|
|
358
|
-
|
|
381
|
+
writeVaultFileSync(vaultBase, join(dir, 'README.md'), body, 'utf8', { label: 'README de specs' });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function assertSpecPromotionTargetsSafe(vaultBase, changeDir, specs) {
|
|
385
|
+
const loc = getLocale(vaultBase);
|
|
386
|
+
const specsRoot = join(vaultBase, loc.folders.specs);
|
|
387
|
+
const checkedRoot = assertVaultPathSafe(vaultBase, specsRoot, {
|
|
388
|
+
expectedType: 'directory', label: 'raiz de specs consolidadas',
|
|
389
|
+
});
|
|
390
|
+
const targets = [
|
|
391
|
+
{ path: join(specsRoot, 'README.md'), expectedType: 'file', label: 'README de specs' },
|
|
392
|
+
{ path: join(vaultBase, '.brain'), expectedType: 'directory', label: 'raiz do estado de specs' },
|
|
393
|
+
{ path: join(vaultBase, SPECS_STATE_FILE), expectedType: 'file', label: 'estado consolidado de specs' },
|
|
394
|
+
];
|
|
395
|
+
if (checkedRoot.exists) {
|
|
396
|
+
for (const name of readdirSync(checkedRoot.target)) {
|
|
397
|
+
if (!name.endsWith('.md')) continue;
|
|
398
|
+
targets.push({
|
|
399
|
+
path: join(checkedRoot.target, name),
|
|
400
|
+
allowMissing: false,
|
|
401
|
+
expectedType: 'file',
|
|
402
|
+
label: `spec consolidada existente ${name}`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const capability of specs) {
|
|
407
|
+
targets.push(
|
|
408
|
+
{
|
|
409
|
+
path: join(changeDir, 'specs', capability, 'spec.md'),
|
|
410
|
+
allowMissing: false,
|
|
411
|
+
expectedType: 'file',
|
|
412
|
+
label: `delta da spec ${capability}`,
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
path: join(specsRoot, `${capability}.md`),
|
|
416
|
+
expectedType: 'file',
|
|
417
|
+
label: `spec consolidada ${capability}`,
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
assertVaultPathsSafe(vaultBase, targets);
|
|
422
|
+
return { specsRoot: checkedRoot.target };
|
|
359
423
|
}
|
|
360
424
|
|
|
361
425
|
// Merge each capability's delta (in the change) into the living spec in 07-Specs.
|
|
@@ -364,6 +428,7 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
364
428
|
const specsDir = loc.folders.specs;
|
|
365
429
|
const promoted = [];
|
|
366
430
|
const warnings = [];
|
|
431
|
+
const { specsRoot } = assertSpecPromotionTargetsSafe(vaultBase, changeDir, specs);
|
|
367
432
|
const state = checkSpecsState(vaultBase);
|
|
368
433
|
const unmanaged = state.missing ? [] : state.changed.filter((capability) => specs.includes(capability));
|
|
369
434
|
if (unmanaged.length) {
|
|
@@ -371,6 +436,7 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
371
436
|
}
|
|
372
437
|
const conflicts = specConflicts(vaultBase, changeDir, specs);
|
|
373
438
|
if (conflicts.length) throw new Error(`conflito de spec: ${conflicts.join('; ')} — reconcilie o delta e rode \`wendkeep spec rebase --change <slug> --accept-current\``);
|
|
439
|
+
const materialized = [];
|
|
374
440
|
for (const cap of specs) {
|
|
375
441
|
let deltaMd;
|
|
376
442
|
try { deltaMd = readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8'); }
|
|
@@ -382,10 +448,23 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
382
448
|
try { current = parseRequirements(readFileSync(livePath, 'utf8')); } catch { /* nova capability */ }
|
|
383
449
|
const applied = applyDelta(current, delta);
|
|
384
450
|
warnings.push(...applied.warnings.map((w) => `${cap}: ${w}`));
|
|
385
|
-
ensureDir(join(vaultBase, specsDir));
|
|
386
451
|
const footer = changeWikilink ? `Atualizado por ${changeWikilink} em ${dateStr}.` : '';
|
|
387
|
-
|
|
388
|
-
|
|
452
|
+
materialized.push({
|
|
453
|
+
capability: cap,
|
|
454
|
+
livePath,
|
|
455
|
+
content: renderSpec(cap, applied.reqs, { footer, reqHeading: loc.reqHeading }),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
mkdirVaultPath(vaultBase, specsRoot, { label: 'raiz de specs consolidadas' });
|
|
459
|
+
for (const item of materialized) {
|
|
460
|
+
writeVaultFileSync(
|
|
461
|
+
vaultBase,
|
|
462
|
+
item.livePath,
|
|
463
|
+
item.content,
|
|
464
|
+
'utf8',
|
|
465
|
+
{ label: `spec consolidada ${item.capability}` },
|
|
466
|
+
);
|
|
467
|
+
promoted.push(item.capability);
|
|
389
468
|
}
|
|
390
469
|
recordPromotedSpecs(vaultBase, promoted);
|
|
391
470
|
ensureSpecsReadme(vaultBase); // self-heal the explainer so existing vaults get it on archive
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -20,7 +20,10 @@ export function refreshSubagents(vaultBase, input) {
|
|
|
20
20
|
if (!sessionRel) return false;
|
|
21
21
|
const sessionPath = join(vaultBase, sessionRel);
|
|
22
22
|
if (!existsSync(sessionPath)) return false;
|
|
23
|
-
updateSessionObservability({
|
|
23
|
+
updateSessionObservability({
|
|
24
|
+
vaultBase, sessionPath, transcriptPath, caller: 'subagent-stop',
|
|
25
|
+
canonicalConversationId: identity.canonicalConversationId,
|
|
26
|
+
});
|
|
24
27
|
return true;
|
|
25
28
|
}
|
|
26
29
|
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -340,7 +340,7 @@ function upsertSection(content, heading, body) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
// Stop-hook entry: scan the session's subagents/workflows, fold into the note. Fail-open.
|
|
343
|
-
export function upsertSubagentUsage(sessionPath, transcriptPath, { lockTimeoutMs } = {}) {
|
|
343
|
+
export function upsertSubagentUsage(sessionPath, transcriptPath, { lockTimeoutMs, vaultBase = '' } = {}) {
|
|
344
344
|
if (!sessionPath || !existsSync(sessionPath)) return false;
|
|
345
345
|
const collected = collectSubagentUsage(sessionDirFromTranscript(transcriptPath));
|
|
346
346
|
if (!collected) return false;
|
|
@@ -377,6 +377,6 @@ export function upsertSubagentUsage(sessionPath, transcriptPath, { lockTimeoutMs
|
|
|
377
377
|
};
|
|
378
378
|
content = setFrontmatterField(content, 'custo_por_modelo_json', `'${JSON.stringify(ledger).replaceAll("'", "''")}'`);
|
|
379
379
|
return upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
|
|
380
|
-
}, lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {});
|
|
380
|
+
}, { ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}), vaultBase });
|
|
381
381
|
return outcome.written;
|
|
382
382
|
}
|
package/hooks/task-log.mjs
CHANGED
|
@@ -52,7 +52,9 @@ export function logTask(vaultBase, input) {
|
|
|
52
52
|
|
|
53
53
|
const heading = getLocale(vaultBase).id === 'en' ? 'Plan progress' : 'Progresso do plano';
|
|
54
54
|
const line = `- [x] ${formatHourMinute(new Date()).replace('-', ':')} ${text}`;
|
|
55
|
-
return mutateSessionNote(sessionPath, (content) => appendProgress(content, line, heading)
|
|
55
|
+
return mutateSessionNote(sessionPath, (content) => appendProgress(content, line, heading), {
|
|
56
|
+
vaultBase,
|
|
57
|
+
}).written;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -986,7 +986,7 @@ export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcr
|
|
|
986
986
|
result = collectSessionUsage({ sessionContent, transcriptPath });
|
|
987
987
|
if (!result) return null; // sem usage OU conteúdo corrompido: não grava
|
|
988
988
|
return upsertUsageSection(result.content, buildUsageSection(result.aggregate, result.entries, result.summary));
|
|
989
|
-
}, lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {});
|
|
989
|
+
}, { ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}), vaultBase });
|
|
990
990
|
return outcome.written || outcome.reason === 'unchanged' ? result : null;
|
|
991
991
|
}
|
|
992
992
|
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
import { getLocale } from './locale.mjs';
|
|
14
14
|
import { parseSharedMemory, validateMemoryEvent } from './memory-schema.mjs';
|
|
15
15
|
import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import { deriveMemoryProjection } from './memory-store.mjs';
|
|
17
|
+
import { assertVaultPathSafe, assertVaultPathsSafe } from './vault-path-safety.mjs';
|
|
17
18
|
import { validateMemoryBundle } from '../src/validate-memory.mjs';
|
|
18
19
|
|
|
19
20
|
const DEFAULT_PENDING_PATTERNS = [
|
|
@@ -100,10 +101,21 @@ function linkedNotesFromSession(content) {
|
|
|
100
101
|
const MEMORY_STATUS_COMMAND = 'wendkeep memory status --gate --vault <vault>';
|
|
101
102
|
const MEMORY_REPAIR_COMMAND = 'wendkeep memory repair --vault <vault>';
|
|
102
103
|
|
|
103
|
-
function readJsonLines(path, label) {
|
|
104
|
-
|
|
104
|
+
function readJsonLines(vaultBase, path, label) {
|
|
105
|
+
let checked;
|
|
106
|
+
try {
|
|
107
|
+
checked = assertVaultPathSafe(vaultBase, path, { expectedType: 'file', label });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { items: [], errors: [`${label} inseguro: ${error?.message || error}`] };
|
|
110
|
+
}
|
|
111
|
+
if (!checked.exists) return { items: [], errors: [] };
|
|
105
112
|
let raw;
|
|
106
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
checked = assertVaultPathSafe(vaultBase, checked.target, {
|
|
115
|
+
allowMissing: false, expectedType: 'file', label,
|
|
116
|
+
});
|
|
117
|
+
raw = readFileSync(checked.target, 'utf8').replace(/\r\n/g, '\n');
|
|
118
|
+
}
|
|
107
119
|
catch (error) { return { items: [], errors: [`${label} ilegível: ${error?.message || error}`] }; }
|
|
108
120
|
const lines = raw.endsWith('\n') ? raw.split('\n').slice(0, -1) : raw.split('\n');
|
|
109
121
|
const items = [];
|
|
@@ -121,45 +133,260 @@ function readJsonLines(path, label) {
|
|
|
121
133
|
|
|
122
134
|
function inspectOutbox(vaultBase, projectId) {
|
|
123
135
|
const dir = join(vaultBase, '.brain', 'memory-outbox');
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
let checked;
|
|
137
|
+
try {
|
|
138
|
+
checked = assertVaultPathSafe(vaultBase, dir, {
|
|
139
|
+
expectedType: 'directory', label: 'outbox de memória',
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
count: 0,
|
|
144
|
+
errors: [`outbox insegura: ${error?.message || error}`],
|
|
145
|
+
eventIds: new Set(),
|
|
146
|
+
eventsById: new Map(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!checked.exists) return {
|
|
150
|
+
count: 0, errors: [], eventIds: new Set(), eventsById: new Map(),
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
checked = assertVaultPathSafe(vaultBase, checked.target, {
|
|
154
|
+
allowMissing: false, expectedType: 'directory', label: 'outbox de memória',
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
count: 0,
|
|
159
|
+
errors: [`outbox insegura: ${error?.message || error}`],
|
|
160
|
+
eventIds: new Set(),
|
|
161
|
+
eventsById: new Map(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const files = readdirSync(checked.target).filter((name) => name.endsWith('.json')).sort();
|
|
126
165
|
const errors = [];
|
|
166
|
+
const eventIds = new Set();
|
|
167
|
+
const eventsById = new Map();
|
|
127
168
|
for (const name of files) {
|
|
128
|
-
const path = join(
|
|
169
|
+
const path = join(checked.target, name);
|
|
129
170
|
try {
|
|
130
|
-
const
|
|
171
|
+
const file = assertVaultPathSafe(vaultBase, path, {
|
|
172
|
+
allowMissing: false, expectedType: 'file', label: `evento ${name} da outbox`,
|
|
173
|
+
});
|
|
174
|
+
const event = JSON.parse(readFileSync(file.target, 'utf8'));
|
|
131
175
|
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
132
176
|
if (!validation.ok) errors.push(`${name}: ${validation.errors.join(' ')}`);
|
|
177
|
+
else {
|
|
178
|
+
eventIds.add(event.event_id);
|
|
179
|
+
eventsById.set(event.event_id, event);
|
|
180
|
+
}
|
|
133
181
|
} catch (error) {
|
|
134
182
|
errors.push(`${name}: JSON inválido: ${error.message}`);
|
|
135
183
|
}
|
|
136
184
|
}
|
|
137
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
count: files.length, errors, eventIds, eventsById,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function memoryMetrics() {
|
|
191
|
+
return {
|
|
192
|
+
schemaVersion: null,
|
|
193
|
+
revision: null,
|
|
194
|
+
eventCursor: null,
|
|
195
|
+
stateHash: null,
|
|
196
|
+
ledgerEvents: 0,
|
|
197
|
+
pendingOutbox: 0,
|
|
198
|
+
candidates: 0,
|
|
199
|
+
activeConflicts: 0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function blockedMemoryBoundary(error) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
status: 'blocked',
|
|
207
|
+
failures: [`Boundary física da memória insegura: ${error?.message || error}`],
|
|
208
|
+
warnings: [],
|
|
209
|
+
metrics: memoryMetrics(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function preflightMemoryBundle(vaultBase) {
|
|
214
|
+
const brain = join(vaultBase, '.brain');
|
|
215
|
+
assertVaultPathSafe(vaultBase, brain, {
|
|
216
|
+
expectedType: 'directory', label: 'raiz .brain da memória',
|
|
217
|
+
});
|
|
218
|
+
assertVaultPathsSafe(vaultBase, [
|
|
219
|
+
'PROJECT.json', 'CORE.md', 'MEMORY_EVENTS.jsonl', 'SHARED_MEMORY.md',
|
|
220
|
+
'MEMORY_CANDIDATES.jsonl',
|
|
221
|
+
].map((name) => ({
|
|
222
|
+
path: join(brain, name), expectedType: 'file', label: `${name} ilegível ou inseguro`,
|
|
223
|
+
})));
|
|
224
|
+
const outbox = assertVaultPathSafe(vaultBase, join(brain, 'memory-outbox'), {
|
|
225
|
+
expectedType: 'directory', label: 'outbox de memória',
|
|
226
|
+
});
|
|
227
|
+
if (!outbox.exists) return;
|
|
228
|
+
const entries = readdirSync(outbox.target);
|
|
229
|
+
for (const name of entries) {
|
|
230
|
+
assertVaultPathSafe(vaultBase, join(outbox.target, name), {
|
|
231
|
+
allowMissing: false, label: `entrada ${name} da outbox de memória`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function checkpointMatchesLedgerPrefix(checkpoint, eventIds, ledgerEvents, vaultBase) {
|
|
237
|
+
if (!checkpoint || typeof checkpoint !== 'object') return false;
|
|
238
|
+
if (!Number.isInteger(checkpoint.revision) || checkpoint.revision < 0) return false;
|
|
239
|
+
if (typeof checkpoint.event_cursor !== 'string' || !checkpoint.event_cursor) return false;
|
|
240
|
+
if (typeof checkpoint.state_hash !== 'string' || !checkpoint.state_hash) return false;
|
|
241
|
+
|
|
242
|
+
const cursorIndex = ledgerEvents.findIndex((event) => event?.event_id === checkpoint.event_cursor);
|
|
243
|
+
if (cursorIndex < 0) return false;
|
|
244
|
+
const prefix = ledgerEvents.slice(0, cursorIndex + 1);
|
|
245
|
+
const prefixIds = new Set(prefix.map((event) => event.event_id));
|
|
246
|
+
if (eventIds.some((eventId) => !prefixIds.has(eventId))) return false;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const replay = deriveMemoryProjection(vaultBase, prefix);
|
|
250
|
+
const causalMatches = checkpoint.causal_event_cursor === undefined
|
|
251
|
+
|| checkpoint.causal_event_cursor === replay.eventCursor;
|
|
252
|
+
return checkpoint.revision === replay.checkpoint.revision
|
|
253
|
+
&& checkpoint.event_cursor === replay.checkpoint.event_cursor
|
|
254
|
+
&& checkpoint.state_hash === replay.checkpoint.state_hash
|
|
255
|
+
&& causalMatches;
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkMemoryAttempts(registry, {
|
|
262
|
+
vaultBase, ledgerEvents = [], outboxEventIds = new Set(), outboxEventsById = new Map(),
|
|
263
|
+
} = {}) {
|
|
264
|
+
const failures = [];
|
|
265
|
+
const warnings = [];
|
|
266
|
+
const ledgerEventIds = new Set(ledgerEvents.map((event) => event?.event_id).filter(Boolean));
|
|
267
|
+
const ledgerById = new Map(ledgerEvents
|
|
268
|
+
.filter((event) => event?.event_id)
|
|
269
|
+
.map((event) => [event.event_id, event]));
|
|
270
|
+
const attempts = Object.entries(registry?.sessions || {})
|
|
271
|
+
.map(([sessionId, entry]) => [sessionId, entry?.last_memory_attempt])
|
|
272
|
+
.filter(([, attempt]) => attempt && typeof attempt === 'object' && attempt.memory_mode === 'v2');
|
|
273
|
+
|
|
274
|
+
for (const [sessionId, attempt] of attempts) {
|
|
275
|
+
const state = String(attempt.state || '');
|
|
276
|
+
const disposition = String(attempt.disposition || '');
|
|
277
|
+
const eventIds = Array.isArray(attempt.event_ids)
|
|
278
|
+
? [...new Set(attempt.event_ids.filter((eventId) => typeof eventId === 'string' && eventId))]
|
|
279
|
+
: [];
|
|
280
|
+
|
|
281
|
+
if (state === 'skipped' && disposition === 'ambiguous') {
|
|
282
|
+
failures.push(`Lifecycle de memória v2 ambíguo: Stop pulou a publicação sem identidade causal suficiente. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (state === 'skipped' && ['stale_turn', 'superseded'].includes(disposition)) {
|
|
287
|
+
if (eventIds.length) {
|
|
288
|
+
failures.push(`Stop stale/superseded emitiu event_ids apesar da rejeição causal. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
289
|
+
} else {
|
|
290
|
+
warnings.push('Stop stale/superseded foi descartado sem publicar memória.');
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (disposition !== 'applied') {
|
|
296
|
+
if (state === 'skipped') warnings.push('Attempt de memória v2 foi descartado sem publicação.');
|
|
297
|
+
else failures.push(`Attempt de memória v2 possui disposition não reconhecida para o estado informado. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!eventIds.length) {
|
|
302
|
+
failures.push(`Attempt v2 aplicado não declarou event_ids; publicação perdida. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const identity = {
|
|
307
|
+
canonical_session_id: sessionId,
|
|
308
|
+
activation_id: attempt.activation_id,
|
|
309
|
+
activation_epoch: attempt.activation_epoch,
|
|
310
|
+
source_turn_id: attempt.turn_id,
|
|
311
|
+
turn_sequence: attempt.turn_sequence,
|
|
312
|
+
};
|
|
313
|
+
const invalidAttemptFields = [];
|
|
314
|
+
if (attempt.canonical_session_id !== sessionId) invalidAttemptFields.push('canonical_session_id');
|
|
315
|
+
if (typeof attempt.activation_id !== 'string' || !attempt.activation_id) invalidAttemptFields.push('activation_id');
|
|
316
|
+
if (!Number.isInteger(attempt.activation_epoch) || attempt.activation_epoch < 0) invalidAttemptFields.push('activation_epoch');
|
|
317
|
+
if (typeof attempt.turn_id !== 'string' || !attempt.turn_id) invalidAttemptFields.push('turn_id');
|
|
318
|
+
if (!Number.isInteger(attempt.turn_sequence) || attempt.turn_sequence < 0) invalidAttemptFields.push('turn_sequence');
|
|
319
|
+
if (invalidAttemptFields.length) {
|
|
320
|
+
failures.push(`Attempt v2 da sessão ${sessionId} possui identidade causal inválida (${invalidAttemptFields.join(', ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const causalMismatches = [];
|
|
324
|
+
for (const eventId of eventIds) {
|
|
325
|
+
const event = ledgerById.get(eventId) || outboxEventsById.get(eventId);
|
|
326
|
+
if (!event) continue;
|
|
327
|
+
const fields = Object.entries(identity)
|
|
328
|
+
.filter(([field, expected]) => event[field] !== expected)
|
|
329
|
+
.map(([field]) => field);
|
|
330
|
+
if (fields.length) causalMismatches.push(`${eventId}: ${fields.join(', ')}`);
|
|
331
|
+
}
|
|
332
|
+
if (causalMismatches.length) {
|
|
333
|
+
failures.push(`Attempt v2 da sessão ${sessionId} referencia evento(s) com identidade causal divergente (${causalMismatches.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (state === 'enqueued' || state === 'degraded') {
|
|
338
|
+
const missing = eventIds.filter((eventId) => !ledgerEventIds.has(eventId) && !outboxEventIds.has(eventId));
|
|
339
|
+
if (missing.length) {
|
|
340
|
+
failures.push(`Attempt v2 perdeu ${missing.length} evento(s): ausentes do ledger e da outbox. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
341
|
+
} else {
|
|
342
|
+
warnings.push(`Attempt de memória v2 ${state} permanece recuperável: ${eventIds.length} evento(s) durável(is) no ledger e/ou outbox.`);
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (state === 'projected') {
|
|
348
|
+
const outsideLedger = eventIds.filter((eventId) => !ledgerEventIds.has(eventId));
|
|
349
|
+
if (outsideLedger.length) {
|
|
350
|
+
failures.push(`Attempt projetado perdeu ${outsideLedger.length} evento(s) no ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
351
|
+
} else if (!checkpointMatchesLedgerPrefix(attempt.checkpoint, eventIds, ledgerEvents, vaultBase)) {
|
|
352
|
+
failures.push(`Checkpoint do attempt projetado diverge do prefixo rederivado do ledger. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
failures.push(`Attempt de memória v2 possui state inválido. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { failures, warnings };
|
|
138
361
|
}
|
|
139
362
|
|
|
140
363
|
/**
|
|
141
364
|
* Read-only consistency check for the local memory-v2 bundle. It intentionally
|
|
142
365
|
* does not acquire MEMORY.lock or invoke the projector/repair paths.
|
|
143
366
|
*/
|
|
144
|
-
export function checkMemoryBundle(vaultBase) {
|
|
367
|
+
export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
368
|
+
if (!existsSync(vaultBase)) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
status: 'blocked',
|
|
372
|
+
failures: [`Vault not found: ${vaultBase}`],
|
|
373
|
+
warnings: [],
|
|
374
|
+
metrics: memoryMetrics(),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
try { preflightMemoryBundle(vaultBase); }
|
|
378
|
+
catch (error) { return blockedMemoryBoundary(error); }
|
|
145
379
|
const brain = join(vaultBase, '.brain');
|
|
146
|
-
|
|
380
|
+
let mode;
|
|
381
|
+
try { mode = detectMemoryMode(vaultBase); }
|
|
382
|
+
catch (error) { return blockedMemoryBoundary(error); }
|
|
147
383
|
if (mode.mode === 'legacy') {
|
|
148
384
|
return {
|
|
149
385
|
ok: true,
|
|
150
386
|
status: 'legacy',
|
|
151
387
|
failures: [],
|
|
152
388
|
warnings: [LEGACY_MEMORY_WARNING],
|
|
153
|
-
metrics:
|
|
154
|
-
schemaVersion: null,
|
|
155
|
-
revision: null,
|
|
156
|
-
eventCursor: null,
|
|
157
|
-
stateHash: null,
|
|
158
|
-
ledgerEvents: 0,
|
|
159
|
-
pendingOutbox: 0,
|
|
160
|
-
candidates: 0,
|
|
161
|
-
activeConflicts: 0,
|
|
162
|
-
},
|
|
389
|
+
metrics: memoryMetrics(),
|
|
163
390
|
};
|
|
164
391
|
}
|
|
165
392
|
const bundle = validateMemoryBundle(vaultBase);
|
|
@@ -170,7 +397,9 @@ export function checkMemoryBundle(vaultBase) {
|
|
|
170
397
|
: { metadata: {} };
|
|
171
398
|
const metadata = parsedShared.metadata || {};
|
|
172
399
|
const outbox = inspectOutbox(vaultBase, bundle.project?.projectId);
|
|
173
|
-
const candidates = readJsonLines(
|
|
400
|
+
const candidates = readJsonLines(
|
|
401
|
+
vaultBase, join(brain, 'MEMORY_CANDIDATES.jsonl'), 'MEMORY_CANDIDATES.jsonl',
|
|
402
|
+
);
|
|
174
403
|
|
|
175
404
|
const ledgerCorrupt = (bundle.ledger?.errors || []).length > 0;
|
|
176
405
|
if (ledgerCorrupt) {
|
|
@@ -192,7 +421,7 @@ export function checkMemoryBundle(vaultBase) {
|
|
|
192
421
|
|
|
193
422
|
let replay = null;
|
|
194
423
|
if (bundle.ledger?.ok) {
|
|
195
|
-
try { replay =
|
|
424
|
+
try { replay = deriveMemoryProjection(vaultBase, bundle.ledger.events); }
|
|
196
425
|
catch (error) {
|
|
197
426
|
failures.push(`Ledger não pode ser reduzido: ${error.message}. Execute com segurança: ${MEMORY_REPAIR_COMMAND}.`);
|
|
198
427
|
}
|
|
@@ -200,13 +429,27 @@ export function checkMemoryBundle(vaultBase) {
|
|
|
200
429
|
if (replay && bundle.shared?.ok) {
|
|
201
430
|
const divergences = [];
|
|
202
431
|
if (metadata.revision !== replay.revision) divergences.push(`revision ${metadata.revision} != ${replay.revision}`);
|
|
203
|
-
if (metadata.event_cursor !== replay.
|
|
432
|
+
if (metadata.event_cursor !== replay.ledgerCursor) divergences.push(`event_cursor ${metadata.event_cursor} != ${replay.ledgerCursor}`);
|
|
204
433
|
if (metadata.state_hash !== replay.stateHash) divergences.push(`state_hash ${metadata.state_hash} != ${replay.stateHash}`);
|
|
205
434
|
if (divergences.length) {
|
|
206
435
|
failures.push(`Projeção SHARED stale/lag (${divergences.join('; ')}). Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
|
|
207
436
|
}
|
|
208
437
|
}
|
|
209
438
|
|
|
439
|
+
let effectiveRegistry = registry;
|
|
440
|
+
if (!effectiveRegistry) {
|
|
441
|
+
try { effectiveRegistry = readSessionRegistry(vaultBase); }
|
|
442
|
+
catch (error) { failures.push(`SESSION_REGISTRY.json inseguro ou ilegível: ${error?.message || error}.`); }
|
|
443
|
+
}
|
|
444
|
+
const lifecycle = checkMemoryAttempts(effectiveRegistry || { version: 2, sessions: {} }, {
|
|
445
|
+
vaultBase,
|
|
446
|
+
ledgerEvents: bundle.ledger?.events || [],
|
|
447
|
+
outboxEventIds: outbox.eventIds,
|
|
448
|
+
outboxEventsById: outbox.eventsById,
|
|
449
|
+
});
|
|
450
|
+
failures.push(...lifecycle.failures);
|
|
451
|
+
warnings.push(...lifecycle.warnings);
|
|
452
|
+
|
|
210
453
|
const unresolved = candidates.items.filter((item) => !['resolved', 'rejected', 'superseded'].includes(item?.status));
|
|
211
454
|
const activeConflicts = unresolved.filter((item) => item?.reason === 'conflict');
|
|
212
455
|
const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
|
|
@@ -332,7 +575,7 @@ export function runVaultHealth({ vaultBase, session = '' }) {
|
|
|
332
575
|
];
|
|
333
576
|
let memory = { status: 'legacy', metrics: {} };
|
|
334
577
|
if (memoryMarkers.some((path) => existsSync(path))) {
|
|
335
|
-
memory = checkMemoryBundle(vaultBase);
|
|
578
|
+
memory = checkMemoryBundle(vaultBase, { registry });
|
|
336
579
|
failures.push(...memory.failures.map((item) => `Memória: ${item}`));
|
|
337
580
|
warnings.push(...memory.warnings.map((item) => `Memória: ${item}`));
|
|
338
581
|
} else {
|