wendkeep 0.68.6 → 0.70.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 +23 -0
- package/README.en.md +7 -3
- package/README.md +7 -3
- package/docs/en/commands/changes-and-verification.md +4 -0
- package/docs/en/commands/costs-and-observability.md +11 -2
- package/docs/en/commands/memory.md +10 -1
- package/docs/en/commands/observer.md +104 -0
- package/docs/pt-BR/commands/changes-and-verification.md +4 -0
- package/docs/pt-BR/commands/costs-and-observability.md +12 -2
- package/docs/pt-BR/commands/memory.md +10 -1
- package/docs/pt-BR/commands/observer.md +105 -0
- package/hooks/brain-core.mjs +46 -2
- package/hooks/brain-inject.mjs +3 -3
- package/hooks/harness-doctor.mjs +21 -7
- package/hooks/observer-publish.mjs +21 -0
- package/hooks/pricing.json +10 -1
- package/hooks/session-ensure.mjs +23 -0
- package/hooks/session-identity.mjs +4 -2
- package/hooks/session-stop.mjs +17 -0
- package/hooks/token-usage.mjs +13 -0
- package/hooks/vault-health.mjs +13 -0
- package/package.json +3 -3
- package/packages/cli/src/index.mjs +9 -2
- package/packages/integrations/src/host-hooks.mjs +4 -0
- package/packages/vault/src/memory-handoff.mjs +75 -9
- package/packages/vault/src/memory-store.mjs +34 -6
- package/packages/vault/src/validate-core.mjs +29 -15
- package/packages/vault/src/validate-memory.mjs +209 -6
- package/src/doctor.mjs +4 -0
- package/src/memory.mjs +4 -1
- package/src/observer-publish.mjs +122 -0
- package/src/observer-server.mjs +203 -0
- package/src/observer-snapshot.mjs +153 -0
- package/src/observer-store.mjs +155 -0
- package/src/observer.mjs +108 -0
- package/src/taxonomy.mjs +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { debugLog, readHookInput, resolveVault } from './obsidian-common.mjs';
|
|
4
|
+
import { publishObserverSnapshot } from '../src/observer-publish.mjs';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const input = readHookInput();
|
|
8
|
+
const resolved = resolveVault(input);
|
|
9
|
+
const result = await publishObserverSnapshot({
|
|
10
|
+
vaultBase: resolved.base,
|
|
11
|
+
projectRoot: resolved.projectRoot,
|
|
12
|
+
});
|
|
13
|
+
if (!result.ok && result.error) debugLog('Observer publish fail-open:', result.error);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
17
|
+
main().catch((error) => {
|
|
18
|
+
debugLog('Observer hook falhou de forma fail-open:', error);
|
|
19
|
+
process.exitCode = 0;
|
|
20
|
+
});
|
|
21
|
+
}
|
package/hooks/pricing.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_nota": "Preços API por milhão de tokens. cachedInput = cache read. Cache write aplica multiplicador no código: 5m = 1.25x input, 1h = 2x input. Editar aqui quando o provedor mudar preços (sem mexer no .mjs). Se o arquivo sumir ou ficar inválido, o hook usa a tabela embutida em token-usage.mjs.",
|
|
3
|
-
"_fonte": "OpenAI https://openai.com/index/gpt-5-6/ e https://help.openai.com/en/articles/
|
|
3
|
+
"_fonte": "OpenAI https://openai.com/index/gpt-5-6/, https://openai.com/index/introducing-gpt-5-3-codex-spark/ e https://help.openai.com/en/articles/20001106-codex-rate-card; Anthropic https://www.anthropic.com/pricing — conferido 2026-08-16",
|
|
4
4
|
"models": {
|
|
5
5
|
"gpt-5.6-sol": { "label": "GPT-5.6 Sol API", "provider": "openai", "input": 5, "cachedInput": 0.5, "output": 30 },
|
|
6
6
|
"gpt-5.6-terra": { "label": "GPT-5.6 Terra API", "provider": "openai", "input": 2.5, "cachedInput": 0.25, "output": 15 },
|
|
@@ -12,6 +12,15 @@
|
|
|
12
12
|
"cachedInput": 0.5,
|
|
13
13
|
"output": 30
|
|
14
14
|
},
|
|
15
|
+
"gpt-5.3-codex-spark": {
|
|
16
|
+
"label": "GPT-5.3-Codex-Spark (research preview)",
|
|
17
|
+
"provider": "openai",
|
|
18
|
+
"pricingStatus": "research-preview",
|
|
19
|
+
"pricingNote": "Tarifa final não publicada; custo não estimado.",
|
|
20
|
+
"input": null,
|
|
21
|
+
"cachedInput": null,
|
|
22
|
+
"output": null
|
|
23
|
+
},
|
|
15
24
|
"claude-opus-4.7": {
|
|
16
25
|
"label": "Claude Opus 4.7 API",
|
|
17
26
|
"provider": "anthropic",
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -36,11 +36,29 @@ import { resolveSessionIdentity } from './session-identity.mjs';
|
|
|
36
36
|
import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
|
|
37
37
|
import { mutateSessionNote } from './session-note-io.mjs';
|
|
38
38
|
import { captureProjectScope, projectScopePatch } from './project-scope.mjs';
|
|
39
|
+
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
39
40
|
|
|
40
41
|
function sessionIdFromInput(input) {
|
|
41
42
|
return input.session_id || input.sessionId || input.codex_session_id || '';
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function workSessionIdFromInput(input = {}) {
|
|
46
|
+
const shared = input.shared || input.handoff?.shared;
|
|
47
|
+
const value = input.work_session_id
|
|
48
|
+
|| input.workSessionId
|
|
49
|
+
|| shared?.work_session_id
|
|
50
|
+
|| shared?.workSessionId
|
|
51
|
+
|| input.handoff?.work_session_id
|
|
52
|
+
|| input.handoff?.workSessionId
|
|
53
|
+
|| '';
|
|
54
|
+
return sanitizeMemoryText(value).trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function workSessionPatch(input = {}) {
|
|
58
|
+
const workSessionId = workSessionIdFromInput(input);
|
|
59
|
+
return workSessionId ? { work_session_id: workSessionId } : {};
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
function turnSequenceFromInput(input = {}) {
|
|
45
63
|
const value = input.turn_sequence ?? input.turnSequence;
|
|
46
64
|
const parsed = Number(value);
|
|
@@ -290,6 +308,7 @@ function activateExistingSession({ vaultBase, relPath, startedAt, sessionId, inp
|
|
|
290
308
|
transcript_path: identity.transcriptPath,
|
|
291
309
|
transcript_id: identity.transcriptId,
|
|
292
310
|
provider: identity.provider,
|
|
311
|
+
...workSessionPatch(input),
|
|
293
312
|
...scopePatch,
|
|
294
313
|
...causalTurnPatch(input, now),
|
|
295
314
|
});
|
|
@@ -318,6 +337,7 @@ function createSession({ vaultBase, sessionId, input, now, identity, scopePatch
|
|
|
318
337
|
transcript_path: identity.transcriptPath,
|
|
319
338
|
transcript_id: identity.transcriptId,
|
|
320
339
|
provider: identity.provider,
|
|
340
|
+
...workSessionPatch(input),
|
|
321
341
|
...scopePatch,
|
|
322
342
|
...causalTurnPatch(input, now),
|
|
323
343
|
});
|
|
@@ -361,6 +381,7 @@ function main() {
|
|
|
361
381
|
upsertSessionRegistry(vaultBase, sessionId, {
|
|
362
382
|
transcript_paths: [identity.transcriptPath],
|
|
363
383
|
provider: identity.provider,
|
|
384
|
+
...workSessionPatch(input),
|
|
364
385
|
});
|
|
365
386
|
}
|
|
366
387
|
writeHookOutput({});
|
|
@@ -398,6 +419,7 @@ function main() {
|
|
|
398
419
|
transcript_path: identity.transcriptPath,
|
|
399
420
|
transcript_id: identity.transcriptId,
|
|
400
421
|
provider: identity.provider,
|
|
422
|
+
...workSessionPatch(input),
|
|
401
423
|
...scopePatch,
|
|
402
424
|
...causalTurnPatch(input, now),
|
|
403
425
|
});
|
|
@@ -443,6 +465,7 @@ function main() {
|
|
|
443
465
|
transcript_path: identity.transcriptPath,
|
|
444
466
|
transcript_id: identity.transcriptId,
|
|
445
467
|
provider: identity.provider,
|
|
468
|
+
...workSessionPatch(input),
|
|
446
469
|
...scopePatch,
|
|
447
470
|
...causalTurnPatch(input, now),
|
|
448
471
|
});
|
|
@@ -65,8 +65,10 @@ export function resolveSessionIdentity(vaultBase, input = {}, provider = detectP
|
|
|
65
65
|
export function resolveSessionEntry(vaultBase, input = {}, provider = detectProvider()) {
|
|
66
66
|
const identity = resolveSessionIdentity(vaultBase, input, provider);
|
|
67
67
|
if (identity.state !== 'resolved') return { identity, entry: null };
|
|
68
|
+
const entry = readSessionRegistry(vaultBase).sessions?.[identity.canonicalConversationId] || null;
|
|
69
|
+
const workSessionId = entry?.work_session_id ? String(entry.work_session_id) : '';
|
|
68
70
|
return {
|
|
69
|
-
identity,
|
|
70
|
-
entry
|
|
71
|
+
identity: workSessionId ? { ...identity, work_session_id: workSessionId } : identity,
|
|
72
|
+
entry,
|
|
71
73
|
};
|
|
72
74
|
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -521,6 +521,21 @@ function shouldFinalizeSession() {
|
|
|
521
521
|
return process.env.OBSIDIAN_NO_AUTO_FINALIZE !== '1';
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
function sharedHandoffFromInput(input = {}, entry = {}) {
|
|
525
|
+
const supplied = input.shared || input.handoff?.shared;
|
|
526
|
+
const shared = supplied && typeof supplied === 'object' && !Array.isArray(supplied)
|
|
527
|
+
? { ...supplied }
|
|
528
|
+
: {};
|
|
529
|
+
const workSessionId = shared.work_session_id
|
|
530
|
+
|| shared.workSessionId
|
|
531
|
+
|| input.work_session_id
|
|
532
|
+
|| input.workSessionId
|
|
533
|
+
|| entry?.work_session_id
|
|
534
|
+
|| '';
|
|
535
|
+
if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
|
|
536
|
+
return Object.keys(shared).length ? shared : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
524
539
|
export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
|
|
525
540
|
if (detectMemoryMode(vaultBase).mode === 'legacy') {
|
|
526
541
|
return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
|
|
@@ -1378,6 +1393,7 @@ export async function main({
|
|
|
1378
1393
|
summary: finalSummary,
|
|
1379
1394
|
noteRel: sessionRel,
|
|
1380
1395
|
});
|
|
1396
|
+
const sharedHandoff = sharedHandoffFromInput(input, entry);
|
|
1381
1397
|
memoryHandoff = {
|
|
1382
1398
|
projectId,
|
|
1383
1399
|
identity,
|
|
@@ -1390,6 +1406,7 @@ export async function main({
|
|
|
1390
1406
|
observedAt: turnIdentity.observedAt || new Date(0).toISOString(),
|
|
1391
1407
|
summary: finalSummary,
|
|
1392
1408
|
evidence: memoryEvidence,
|
|
1409
|
+
...(sharedHandoff ? { shared: sharedHandoff } : {}),
|
|
1393
1410
|
};
|
|
1394
1411
|
memoryAttempt = stageMemory(vaultBase, {
|
|
1395
1412
|
handoff: memoryHandoff,
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -30,6 +30,15 @@ const DEFAULT_PRICE_REFERENCE = {
|
|
|
30
30
|
'gpt-5.6-sol': { label: 'GPT-5.6 Sol API', provider: 'openai', input: 5, cachedInput: 0.5, output: 30 },
|
|
31
31
|
'gpt-5.6-terra': { label: 'GPT-5.6 Terra API', provider: 'openai', input: 2.5, cachedInput: 0.25, output: 15 },
|
|
32
32
|
'gpt-5.6-luna': { label: 'GPT-5.6 Luna API', provider: 'openai', input: 1, cachedInput: 0.1, output: 6 },
|
|
33
|
+
'gpt-5.3-codex-spark': {
|
|
34
|
+
label: 'GPT-5.3-Codex-Spark (research preview)',
|
|
35
|
+
provider: 'openai',
|
|
36
|
+
pricingStatus: 'research-preview',
|
|
37
|
+
pricingNote: 'Tarifa final não publicada; custo não estimado.',
|
|
38
|
+
input: null,
|
|
39
|
+
cachedInput: null,
|
|
40
|
+
output: null,
|
|
41
|
+
},
|
|
33
42
|
'gpt-5.5': {
|
|
34
43
|
label: 'GPT-5.5 API',
|
|
35
44
|
provider: 'openai',
|
|
@@ -122,6 +131,10 @@ const MODEL_ALIASES = {
|
|
|
122
131
|
'gpt-5.3-codex': 'gpt-5.5',
|
|
123
132
|
'gpt-5.3': 'gpt-5.5',
|
|
124
133
|
'openai/gpt-5.4': 'gpt-5.5',
|
|
134
|
+
'gpt-5.3-codex-spark': 'gpt-5.3-codex-spark',
|
|
135
|
+
'gpt-5-3-codex-spark': 'gpt-5.3-codex-spark',
|
|
136
|
+
'openai/gpt-5.3-codex-spark': 'gpt-5.3-codex-spark',
|
|
137
|
+
'openai/gpt-5-3-codex-spark': 'gpt-5.3-codex-spark',
|
|
125
138
|
'claude-opus-4.7': 'claude-opus-4.7',
|
|
126
139
|
'claude-opus-4-7': 'claude-opus-4.7',
|
|
127
140
|
'anthropic/claude-opus-4.7': 'claude-opus-4.7',
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -231,6 +231,12 @@ function memoryMetrics() {
|
|
|
231
231
|
pendingOutbox: 0,
|
|
232
232
|
candidates: 0,
|
|
233
233
|
activeConflicts: 0,
|
|
234
|
+
semanticStatus: null,
|
|
235
|
+
semanticCode: null,
|
|
236
|
+
semanticActiveKeys: [],
|
|
237
|
+
semanticProjectedKeys: [],
|
|
238
|
+
semanticMissingKeys: [],
|
|
239
|
+
semanticCounts: {},
|
|
234
240
|
};
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -505,6 +511,7 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
505
511
|
for (const warning of bundle.warnings || []) warnings.push(warning);
|
|
506
512
|
|
|
507
513
|
const ok = failures.length === 0;
|
|
514
|
+
const semantic = bundle.semantic || {};
|
|
508
515
|
return {
|
|
509
516
|
ok,
|
|
510
517
|
status: ok ? (warnings.length ? 'warning' : 'healthy') : 'blocked',
|
|
@@ -519,6 +526,12 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
519
526
|
pendingOutbox: outbox.count,
|
|
520
527
|
candidates: candidates.items.length,
|
|
521
528
|
activeConflicts: activeConflicts.length,
|
|
529
|
+
semanticStatus: semantic.status ?? null,
|
|
530
|
+
semanticCode: semantic.code ?? null,
|
|
531
|
+
semanticActiveKeys: semantic.activeKeys || [],
|
|
532
|
+
semanticProjectedKeys: semantic.projectedKeys || [],
|
|
533
|
+
semanticMissingKeys: semantic.missingKeys || [],
|
|
534
|
+
semanticCounts: semantic.counts || {},
|
|
522
535
|
},
|
|
523
536
|
};
|
|
524
537
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
43
|
+
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/observer-publish.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
44
44
|
"test": "node --test --test-concurrency=2",
|
|
45
45
|
"release": "node scripts/release.mjs",
|
|
46
46
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
@@ -70,6 +70,6 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"acorn": "^8.18.0",
|
|
73
|
-
"wendkeep": "^0.
|
|
73
|
+
"wendkeep": "^0.69.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -49,6 +49,7 @@ Usage:
|
|
|
49
49
|
cannot replace itself. · --vault P · --profile <name> · --yes.
|
|
50
50
|
|
|
51
51
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
52
|
+
wendkeep observer <sub> Local multi-project Observer: serve | register | publish | status.
|
|
52
53
|
wendkeep change <sub> Change lifecycle: new [--simple] | use | bind <slug> --session <id> | continue | list | show |
|
|
53
54
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
54
55
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
@@ -107,7 +108,7 @@ Usage:
|
|
|
107
108
|
promote <candidate> [--event <event-id>] | reject <candidate>. --vault P.
|
|
108
109
|
Reconcile is dry-run by default; the original attempt remains audited.
|
|
109
110
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
110
|
-
protocol (cap
|
|
111
|
+
protocol (cap 40, warning 35, 4 KiB, 320 chars/line, 3 sections, no secrets/PII).
|
|
111
112
|
--vault <path> validates the complete v2 bundle.
|
|
112
113
|
wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
|
|
113
114
|
project: .brain/agents/*.toml -> .codex/agents,
|
|
@@ -199,7 +200,7 @@ async function main(argv) {
|
|
|
199
200
|
// `sync` starts with `init` and resolves the freshly bound Vault itself. Pre-resolving
|
|
200
201
|
// here would prevent that repair step from reporting a corrupt binding as its own
|
|
201
202
|
// first-stage failure (and could never make it as far as the guarded init).
|
|
202
|
-
&& !['init', 'sync', 'hook', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
203
|
+
&& !['init', 'sync', 'hook', 'observer', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
203
204
|
await preferProjectVault(rest);
|
|
204
205
|
}
|
|
205
206
|
switch (cmd) {
|
|
@@ -216,6 +217,12 @@ async function main(argv) {
|
|
|
216
217
|
process.exit(runDoctor(rest));
|
|
217
218
|
break;
|
|
218
219
|
}
|
|
220
|
+
case 'observer': {
|
|
221
|
+
const { runObserver } = await import('../../../src/observer.mjs');
|
|
222
|
+
const observerExitCode = await runObserver(rest);
|
|
223
|
+
if (rest[0] !== 'serve') process.exit(observerExitCode);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
219
226
|
case 'sync': {
|
|
220
227
|
const { runSync } = await import('../../../src/sync.mjs');
|
|
221
228
|
process.exit(await runSync(rest));
|
|
@@ -13,7 +13,11 @@ export const SESSION_HOOKS = [
|
|
|
13
13
|
// memory injection for the whole session.
|
|
14
14
|
{ event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, codex: true, statusMessage: 'wendkeep: injecting memory + active change' },
|
|
15
15
|
{ event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, codex: true, statusMessage: 'wendkeep: opening Obsidian session' },
|
|
16
|
+
// Observer publication is a derived, fail-open projection and therefore runs only after
|
|
17
|
+
// the local lifecycle hook has written its authoritative session state.
|
|
18
|
+
{ event: 'SessionStart', matcher: 'startup|resume|clear|compact', name: 'observer-publish', timeout: 5, order: 20, codex: true, statusMessage: 'wendkeep: publishing local observer snapshot' },
|
|
16
19
|
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
20
|
+
{ event: 'Stop', matcher: null, name: 'observer-publish', timeout: 5, order: 20, codex: true, statusMessage: 'wendkeep: publishing local observer snapshot' },
|
|
17
21
|
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
|
|
18
22
|
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
19
23
|
// codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
|
|
@@ -4,12 +4,55 @@ import { basename, join, relative } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
6
6
|
|
|
7
|
+
const SHARED_HANDOFF_FIELDS = Object.freeze([
|
|
8
|
+
['objective', 'objective.current'],
|
|
9
|
+
['delivered', 'state.delivered'],
|
|
10
|
+
['constraints', 'constraint.active'],
|
|
11
|
+
['decisions', 'decision.active'],
|
|
12
|
+
['next_actions', 'next.action'],
|
|
13
|
+
['blockers', 'blocker.active'],
|
|
14
|
+
['risks', 'risk.known'],
|
|
15
|
+
]);
|
|
16
|
+
|
|
7
17
|
function canonicalValue(value) {
|
|
8
18
|
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
9
19
|
if (!value || typeof value !== 'object') return value;
|
|
10
20
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]));
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
function sanitizeValue(value) {
|
|
24
|
+
if (typeof value === 'string') return sanitizeMemoryText(value);
|
|
25
|
+
if (Array.isArray(value)) return value.map(sanitizeValue);
|
|
26
|
+
if (value && typeof value === 'object') {
|
|
27
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sanitizeValue(value[key])]));
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasMeaningfulValue(value) {
|
|
33
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
34
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
35
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
36
|
+
return value !== undefined && value !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Normalize the portable operational handoff without inventing missing identity. */
|
|
40
|
+
export function normalizeSharedHandoff(shared) {
|
|
41
|
+
if (!shared || typeof shared !== 'object' || Array.isArray(shared)) return null;
|
|
42
|
+
|
|
43
|
+
const normalized = {};
|
|
44
|
+
const workSessionId = sanitizeMemoryText(shared.work_session_id ?? shared.workSessionId ?? '').trim();
|
|
45
|
+
if (workSessionId) normalized.work_session_id = workSessionId;
|
|
46
|
+
|
|
47
|
+
for (const [field] of SHARED_HANDOFF_FIELDS) {
|
|
48
|
+
if (!Object.hasOwn(shared, field)) continue;
|
|
49
|
+
const value = sanitizeValue(shared[field]);
|
|
50
|
+
if (hasMeaningfulValue(value)) normalized[field] = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Object.keys(normalized).length ? normalized : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
function eventId(context, memoryKey, value) {
|
|
14
57
|
const digest = createHash('sha256')
|
|
15
58
|
.update(JSON.stringify([
|
|
@@ -26,8 +69,8 @@ function eventId(context, memoryKey, value) {
|
|
|
26
69
|
}
|
|
27
70
|
|
|
28
71
|
function makeEvent(context, { memoryKey, value, authority, evidence }) {
|
|
29
|
-
const cleanValue =
|
|
30
|
-
|
|
72
|
+
const cleanValue = sanitizeValue(value);
|
|
73
|
+
const event = {
|
|
31
74
|
v: 1,
|
|
32
75
|
event_id: eventId(context, memoryKey, cleanValue),
|
|
33
76
|
project_id: String(context.projectId || ''),
|
|
@@ -43,6 +86,8 @@ function makeEvent(context, { memoryKey, value, authority, evidence }) {
|
|
|
43
86
|
observed_at: context.observedAt,
|
|
44
87
|
evidence: (evidence || []).filter(Boolean).map((item) => sanitizeMemoryText(item)),
|
|
45
88
|
};
|
|
89
|
+
if (context.workSessionId) event.work_session_id = context.workSessionId;
|
|
90
|
+
return event;
|
|
46
91
|
}
|
|
47
92
|
|
|
48
93
|
function readJson(path) {
|
|
@@ -133,14 +178,35 @@ export function buildSessionMemoryEvents({
|
|
|
133
178
|
observedAt,
|
|
134
179
|
summary,
|
|
135
180
|
evidence = {},
|
|
181
|
+
shared,
|
|
136
182
|
}) {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
183
|
+
const normalizedShared = normalizeSharedHandoff(shared);
|
|
184
|
+
const context = {
|
|
185
|
+
projectId, identity, activation, turn, observedAt,
|
|
186
|
+
workSessionId: normalizedShared?.work_session_id || '',
|
|
187
|
+
};
|
|
188
|
+
const events = [];
|
|
189
|
+
|
|
190
|
+
if (normalizedShared) {
|
|
191
|
+
for (const [field, memoryKey] of SHARED_HANDOFF_FIELDS) {
|
|
192
|
+
if (!Object.hasOwn(normalizedShared, field)) continue;
|
|
193
|
+
events.push(makeEvent(context, {
|
|
194
|
+
memoryKey,
|
|
195
|
+
value: normalizedShared[field],
|
|
196
|
+
authority: 'reported',
|
|
197
|
+
evidence: [noteRel],
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!events.length) {
|
|
203
|
+
events.push(makeEvent(context, {
|
|
204
|
+
memoryKey: 'handoff.latest',
|
|
205
|
+
value: sanitizeMemoryText(summary),
|
|
206
|
+
authority: 'reported',
|
|
207
|
+
evidence: [noteRel],
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
144
210
|
|
|
145
211
|
if (evidence.change?.slug && evidence.change?.status && evidence.change?.adr) {
|
|
146
212
|
events.push(makeEvent(context, {
|
|
@@ -437,6 +437,29 @@ function conflictCandidate(memoryKey, events, currentEvent = null) {
|
|
|
437
437
|
};
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
function conflictReviewEvent(candidate, candidateCount = 1) {
|
|
441
|
+
const source = candidate.events?.[0] || {};
|
|
442
|
+
const memoryKey = sanitizeMemoryText(candidate.memory_key || 'unknown');
|
|
443
|
+
const eventCount = Array.isArray(candidate.event_ids) ? candidate.event_ids.length : 0;
|
|
444
|
+
return {
|
|
445
|
+
v: 1,
|
|
446
|
+
event_id: `mem-review-${candidate.candidate_id}`,
|
|
447
|
+
project_id: source.project_id || '',
|
|
448
|
+
memory_key: candidate.memory_key,
|
|
449
|
+
operation: 'assert',
|
|
450
|
+
value: `[revisão pendente: ${memoryKey}; candidates: ${candidateCount}; events: ${eventCount}]`,
|
|
451
|
+
authority: 'candidate',
|
|
452
|
+
canonical_session_id: source.canonical_session_id || 'memory-reducer',
|
|
453
|
+
activation_id: source.activation_id || 'memory-reducer',
|
|
454
|
+
activation_epoch: Number.isInteger(source.activation_epoch) ? source.activation_epoch : 0,
|
|
455
|
+
turn_sequence: 0,
|
|
456
|
+
source_turn_id: 'memory-review',
|
|
457
|
+
observed_at: source.observed_at || new Date(0).toISOString(),
|
|
458
|
+
evidence: ['MEMORY_CANDIDATES.jsonl'],
|
|
459
|
+
review_pending: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
440
463
|
function sortedObject(entries) {
|
|
441
464
|
return Object.fromEntries([...entries].sort(([left], [right]) => left.localeCompare(right)));
|
|
442
465
|
}
|
|
@@ -753,14 +776,19 @@ export function reduceMemoryEvents(inputEvents = [], {
|
|
|
753
776
|
&& !resolvedCandidateIds.has(item.candidate_id));
|
|
754
777
|
unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
|
|
755
778
|
superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
|
|
756
|
-
const activeEvents = Object.entries(recordObject).map(([memoryKey, record]) => ({
|
|
757
|
-
...record.source,
|
|
758
|
-
memory_key: memoryKey,
|
|
759
|
-
operation: 'assert',
|
|
760
|
-
value: record.value,
|
|
761
|
-
}));
|
|
762
779
|
const eventCursor = events.at(-1)?.event_id || 'none';
|
|
763
780
|
const stateHash = hashMemoryValue({ state, tombstones: tombstoneObject });
|
|
781
|
+
const activeEvents = [
|
|
782
|
+
...Object.entries(recordObject).map(([memoryKey, record]) => ({
|
|
783
|
+
...record.source,
|
|
784
|
+
memory_key: memoryKey,
|
|
785
|
+
operation: 'assert',
|
|
786
|
+
value: record.value,
|
|
787
|
+
})),
|
|
788
|
+
...unresolvedCandidates.map((candidate) => (
|
|
789
|
+
conflictReviewEvent(candidate, unresolvedCandidates.length)
|
|
790
|
+
)),
|
|
791
|
+
];
|
|
764
792
|
|
|
765
793
|
return {
|
|
766
794
|
state,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Memory-compaction protocol for the curated .brain/CORE.md layer.
|
|
2
2
|
// Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
|
|
3
|
-
// - cap
|
|
3
|
+
// - cap 40 lines (hard), 35 (soft warning) — 1 durable item per line
|
|
4
|
+
// - 4 KiB and 320 characters per line
|
|
4
5
|
// - 3 required sections
|
|
5
6
|
// - no secrets / no real-provider PII emails
|
|
6
7
|
// Plus the seeded skeleton and the protocol reference doc.
|
|
@@ -8,8 +9,12 @@
|
|
|
8
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
|
|
12
|
+
export const CORE_LIMITS = Object.freeze({
|
|
13
|
+
lines: 40,
|
|
14
|
+
warningLines: 35,
|
|
15
|
+
bytes: 4 * 1024,
|
|
16
|
+
lineChars: 320,
|
|
17
|
+
});
|
|
13
18
|
|
|
14
19
|
// Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
|
|
15
20
|
// locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
|
|
@@ -40,16 +45,25 @@ const SECRET_PATTERNS = [
|
|
|
40
45
|
|
|
41
46
|
const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
|
|
42
47
|
|
|
43
|
-
// Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
|
|
48
|
+
// Validate CORE.md content. Returns { ok, errors, warnings, lineCount, byteCount }.
|
|
44
49
|
export function validateCore(content) {
|
|
45
50
|
const text = String(content ?? '');
|
|
46
51
|
const lines = text.split('\n');
|
|
47
52
|
const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
53
|
+
const byteCount = Buffer.byteLength(text, 'utf8');
|
|
48
54
|
const errors = [];
|
|
49
55
|
|
|
50
|
-
if (lineCount >
|
|
51
|
-
errors.push(`Tamanho ${lineCount} > ${
|
|
56
|
+
if (lineCount > CORE_LIMITS.lines) {
|
|
57
|
+
errors.push(`Tamanho ${lineCount} > ${CORE_LIMITS.lines} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
52
58
|
}
|
|
59
|
+
if (byteCount > CORE_LIMITS.bytes) {
|
|
60
|
+
errors.push(`Tamanho ${byteCount} > ${CORE_LIMITS.bytes} bytes (budget do CORE). Curar: manter apenas estado durável.`);
|
|
61
|
+
}
|
|
62
|
+
lines.forEach((line, index) => {
|
|
63
|
+
if (line.length > CORE_LIMITS.lineChars) {
|
|
64
|
+
errors.push(`Linha ${index + 1} tem ${line.length} caracteres; limite ${CORE_LIMITS.lineChars}.`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
53
67
|
// Pick the locale set that matches best; require it to be complete.
|
|
54
68
|
const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
|
|
55
69
|
const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
|
|
@@ -62,11 +76,11 @@ export function validateCore(content) {
|
|
|
62
76
|
if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
|
|
63
77
|
|
|
64
78
|
const warnings = [];
|
|
65
|
-
if (lineCount >=
|
|
66
|
-
warnings.push(`Tamanho ${lineCount}/${
|
|
79
|
+
if (lineCount >= CORE_LIMITS.warningLines && lineCount <= CORE_LIMITS.lines) {
|
|
80
|
+
warnings.push(`Tamanho ${lineCount}/${CORE_LIMITS.lines} linhas — perto do limite; remover itens resolvidos (≥${CORE_LIMITS.warningLines}).`);
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
return { ok: errors.length === 0, errors, warnings, lineCount };
|
|
83
|
+
return { ok: errors.length === 0, errors, warnings, lineCount, byteCount };
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
@@ -75,7 +89,7 @@ export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
|
75
89
|
if (localeId === 'en') {
|
|
76
90
|
return `# CORE — curated memory core (.brain)
|
|
77
91
|
|
|
78
|
-
> RULE #1 — the project's canonical memory. Hand-curated,
|
|
92
|
+
> RULE #1 — the project's canonical memory. Hand-curated, 40-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
|
|
79
93
|
|
|
80
94
|
## User Preferences
|
|
81
95
|
- (durable preferences: language, style, conventions)
|
|
@@ -89,7 +103,7 @@ export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
|
89
103
|
}
|
|
90
104
|
return `# CORE — núcleo curado da memória (.brain)
|
|
91
105
|
|
|
92
|
-
> REGRA #1 — memória canônica do projeto. Curado à mão, cap
|
|
106
|
+
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 40 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
|
93
107
|
|
|
94
108
|
## Preferências do Usuário
|
|
95
109
|
- (preferências duráveis: idioma, estilo, convenções)
|
|
@@ -110,8 +124,8 @@ export function renderCompactionProtocol() {
|
|
|
110
124
|
|
|
111
125
|
## 1. Duas camadas
|
|
112
126
|
|
|
113
|
-
- **QUENTE** (auto-injetada por sessão,
|
|
114
|
-
- \`.brain/CORE.md\` — curado à mão, **≤
|
|
127
|
+
- **QUENTE** (auto-injetada por sessão, com budgets por camada):
|
|
128
|
+
- \`.brain/CORE.md\` — curado à mão, **≤40 linhas** (alerta em 35; 4 KiB; 320 caracteres por linha): preferências, padrões, pendências.
|
|
115
129
|
- \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
|
|
116
130
|
- **FRIA** (sob demanda):
|
|
117
131
|
- \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
|
|
@@ -120,7 +134,7 @@ export function renderCompactionProtocol() {
|
|
|
120
134
|
## 2. Compactação = regra de geração (sem trabalho manual)
|
|
121
135
|
|
|
122
136
|
- **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
|
|
123
|
-
- **CORE**: quando ≥
|
|
137
|
+
- **CORE**: quando ≥35 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
|
|
124
138
|
|
|
125
139
|
## 3. O que escrever no CORE
|
|
126
140
|
|
|
@@ -139,7 +153,7 @@ wendkeep validate-memory # valida <vault>/.brain/CORE.md
|
|
|
139
153
|
wendkeep validate-memory <path> # valida outro arquivo
|
|
140
154
|
\`\`\`
|
|
141
155
|
|
|
142
|
-
Checa: cap
|
|
156
|
+
Checa: cap 40 (soft 35), 4 KiB, 320 caracteres por linha, 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
|
|
143
157
|
`;
|
|
144
158
|
}
|
|
145
159
|
|