wendkeep 0.76.6 → 0.76.7

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 CHANGED
@@ -4,6 +4,21 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.76.7] — 2026-08-22
8
+
9
+ ### Fixed
10
+
11
+ - **Handoff causal no Stop.** Com `active_contexts` inicializado, work session, repository,
12
+ worktree, branch e change vêm do contexto chamador; payload divergente falha antes de CAS,
13
+ nota, outbox ou ledger e não publica estado parcial.
14
+ - **Evidência da change correta.** O lifecycle coleta verdict, sensores e ADR apenas da change do
15
+ active context causal, sem reutilizar `change_slug` legado ou link global de uma sessão irmã.
16
+ - **Recall automático escopado.** `UserPromptSubmit` permanece somente leitura, sem migrar pointer
17
+ ou registry; exclui evidência de sessão/change irmã ativa e preserva material global/histórico.
18
+ `/brain-recall` explícito segue global.
19
+ - **Compatibilidade conservadora.** O comportamento anterior permanece somente quando o registry
20
+ ainda não possui nenhum campo do store contextual, inclusive quando `active_contexts` está vazio.
21
+
7
22
  ## [0.76.6] — 2026-08-22
8
23
 
9
24
  ### Fixed
package/README.en.md CHANGED
@@ -386,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
386
386
  - **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
387
387
  - **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
388
388
  - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
389
- - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
390
- - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity; `UserPromptSubmit` injects only relevant passages with provenance.
389
+ - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
390
+ - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
391
391
 
392
392
  Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
393
393
 
package/README.md CHANGED
@@ -386,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
386
386
  - **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
387
387
  - **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
388
388
  - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
389
- - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
390
- - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity; `UserPromptSubmit` injects only relevant passages with provenance.
389
+ - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
390
+ - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
391
391
 
392
392
  Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
393
393
 
@@ -115,10 +115,17 @@ npx wendkeep validate-memory --vault <v2-vault>
115
115
  `change.<slug>.status` compete only inside the same scope. Automatic resolution still requires
116
116
  the same project and causal lineage; incompatible decisions, constraints, and blockers remain
117
117
  human-curated. An ambiguous key is omitted from SHARED without removing CORE or independent keys.
118
+ Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`,
119
+ `worktree_id`, branch, and `change_slug` from the causal active context. A divergent handoff
120
+ identity fails before CAS, note, outbox, or ledger mutation; legacy fallback exists only without
121
+ the contextual store.
118
122
  - `.brain/EVIDENCE_INDEX.jsonl` chunks documents by heading and block and records file, heading,
119
123
  type, change, session, work session, authority, date, validity, and hash. `/brain-recall` and the
120
124
  `UserPromptSubmit` hook use BM25, exact phrase, field weights, authority, validity, bounded
121
- recency, and diversity to return the matching passage with provenance.
125
+ recency, and diversity to return the matching passage with provenance. Automatic recall is
126
+ read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes rows owned
127
+ by an active sibling session or change while preserving global or historical rows with no active
128
+ owner. Explicit `/brain-recall` remains global.
122
129
  - Every memory path validates the physical topology of `.brain`, ledger, outbox, CORE, SHARED,
123
130
  candidates, registry, notes, backups, temporary files, and sidecars before reading or writing.
124
131
  Junctions, symlinks, reparse points, or hardlinks fail closed without touching external bytes.
@@ -114,11 +114,17 @@ npx wendkeep validate-memory --vault <cofre-v2>
114
114
  - Registradores como `git.local-head`, `handoff.latest`, `quality.latest-*` e
115
115
  `change.<slug>.status` só competem dentro do mesmo escopo. Resolução automática ainda exige o
116
116
  mesmo projeto e linhagem causal; decisões, constraints e blockers incompatíveis permanecem sob
117
- curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes.
117
+ curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes. Com
118
+ `active_contexts` inicializado, o `Stop` deriva `work_session_id`, `repository_id`, `worktree_id`,
119
+ branch e `change_slug` do active context causal. Identidade divergente no handoff falha antes de
120
+ CAS, nota, outbox ou ledger; o fallback legado só existe sem o store contextual.
118
121
  - `.brain/EVIDENCE_INDEX.jsonl` divide documentos por headings e blocos e registra arquivo,
119
122
  heading, tipo, change, sessão, work session, autoridade, data, validade e hash. `/brain-recall`
120
123
  e o hook `UserPromptSubmit` usam BM25, frase exata, pesos por campo, autoridade, validade,
121
- recência limitada e diversidade para retornar o trecho do match com proveniência.
124
+ recência limitada e diversidade para retornar o trecho do match com proveniência. No recall
125
+ automático, o hook é somente leitura: não migra `CURRENT_CHANGE.md` nem altera o registry;
126
+ exclui linhas pertencentes a sessão ou change irmã ativa e preserva linhas globais ou históricas
127
+ sem owner ativo. `/brain-recall` explícito permanece global.
122
128
  - Toda rota de memória valida a topologia física de `.brain`, ledger, outbox, CORE, SHARED,
123
129
  candidates, registry, notas, backups, temporários e sidecars antes de ler ou escrever. Junction,
124
130
  symlink, reparse point ou hardlink falham fechados sem tocar bytes externos. Locks publicam owner
@@ -0,0 +1,196 @@
1
+ import { readSessionRegistry } from './obsidian-common.mjs';
2
+ import { resolveActiveContext } from './active-context-store.mjs';
3
+ import { resolveRuntimeActiveContext } from '../src/active-context-runtime.mjs';
4
+
5
+ const IDENTITY_FIELDS = Object.freeze([
6
+ ['work_session_id', 'workSessionId'],
7
+ ['repository_id', 'repositoryId'],
8
+ ['worktree_id', 'worktreeId'],
9
+ ['branch', 'branch'],
10
+ ['change_slug', 'changeSlug'],
11
+ ]);
12
+
13
+ function contextError(code, message) {
14
+ const error = new Error(message);
15
+ error.code = code;
16
+ return error;
17
+ }
18
+
19
+ function text(value) {
20
+ return String(value ?? '').trim();
21
+ }
22
+
23
+ function contextsOf(registry) {
24
+ return registry?.active_contexts && typeof registry.active_contexts === 'object'
25
+ && !Array.isArray(registry.active_contexts)
26
+ ? registry.active_contexts : {};
27
+ }
28
+
29
+ export function activeContextRegistryInitialized(registry = {}) {
30
+ return Object.hasOwn(registry, 'active_contexts')
31
+ || Object.hasOwn(registry, 'active_contexts_schema')
32
+ || Object.hasOwn(registry, 'active_contexts_revision');
33
+ }
34
+
35
+ function assertAuthority(identity, context) {
36
+ if (!identity || !context || context.state !== 'active') {
37
+ throw contextError(
38
+ 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
39
+ 'active context causal não foi resolvido antes do Stop',
40
+ );
41
+ }
42
+ for (const [contextField, identityField] of [
43
+ ['project_id', 'projectId'],
44
+ ['repository_id', 'repositoryId'],
45
+ ['worktree_id', 'worktreeId'],
46
+ ['work_session_id', 'workSessionId'],
47
+ ]) {
48
+ if (!text(context[contextField]) || text(context[contextField]) !== text(identity[identityField])) {
49
+ throw contextError(
50
+ 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
51
+ `${contextField} do active context não corresponde à sessão causal`,
52
+ );
53
+ }
54
+ }
55
+ if (!text(context.branch) || text(context.branch) !== text(identity.branch)) {
56
+ throw contextError(
57
+ 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
58
+ 'branch do active context não corresponde à sessão causal',
59
+ );
60
+ }
61
+ }
62
+
63
+ export function resolveHandoffEvidenceAuthority(vaultBase, {
64
+ input = {},
65
+ sessionId = '',
66
+ projectRoot = input.cwd || process.cwd(),
67
+ registry = readSessionRegistry(vaultBase),
68
+ resolveCommand = resolveRuntimeActiveContext,
69
+ resolveContext = resolveActiveContext,
70
+ } = {}) {
71
+ if (!activeContextRegistryInitialized(registry)) {
72
+ return { mode: 'legacy', identity: null, context: null };
73
+ }
74
+ const identity = resolveCommand({
75
+ vaultBase,
76
+ projectRoot,
77
+ sessionId,
78
+ });
79
+ if (!identity) {
80
+ throw contextError(
81
+ 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
82
+ 'registry contextual inicializado sem identidade causal para o Stop',
83
+ );
84
+ }
85
+ const context = resolveContext(vaultBase, { ...identity, sessionId });
86
+ assertAuthority(identity, context);
87
+ return { mode: 'contextual', identity, context };
88
+ }
89
+
90
+ export function resolveReadOnlyEvidenceActiveContext({
91
+ vaultBase,
92
+ projectRoot = process.cwd(),
93
+ sessionId = '',
94
+ } = {}) {
95
+ const authority = resolveHandoffEvidenceAuthority(vaultBase, {
96
+ input: { cwd: projectRoot },
97
+ projectRoot,
98
+ sessionId,
99
+ });
100
+ return authority.mode === 'contextual' ? authority.identity : null;
101
+ }
102
+
103
+ function suppliedShared(input) {
104
+ const supplied = input?.shared || input?.handoff?.shared;
105
+ return supplied && typeof supplied === 'object' && !Array.isArray(supplied)
106
+ ? { ...supplied }
107
+ : {};
108
+ }
109
+
110
+ function suppliedIdentityValues(input, shared, snake, camel) {
111
+ const direct = input?.shared && typeof input.shared === 'object' && !Array.isArray(input.shared)
112
+ ? input.shared : {};
113
+ const nested = input?.handoff?.shared && typeof input.handoff.shared === 'object'
114
+ && !Array.isArray(input.handoff.shared)
115
+ ? input.handoff.shared : {};
116
+ return [
117
+ shared[snake], shared[camel],
118
+ direct[snake], direct[camel],
119
+ nested[snake], nested[camel],
120
+ input?.[snake], input?.[camel],
121
+ ]
122
+ .map(text)
123
+ .filter(Boolean);
124
+ }
125
+
126
+ export function buildCausalSharedHandoff({ input = {}, entry = {}, authority } = {}) {
127
+ const shared = suppliedShared(input);
128
+ if (authority?.mode !== 'contextual') {
129
+ const workSessionId = text(
130
+ shared.work_session_id
131
+ || shared.workSessionId
132
+ || input.work_session_id
133
+ || input.workSessionId
134
+ || entry?.work_session_id,
135
+ );
136
+ if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
137
+ return Object.keys(shared).length ? shared : null;
138
+ }
139
+
140
+ assertAuthority(authority.identity, authority.context);
141
+ const expected = {
142
+ workSessionId: text(authority.identity.workSessionId),
143
+ repositoryId: text(authority.identity.repositoryId),
144
+ worktreeId: text(authority.identity.worktreeId),
145
+ branch: text(authority.identity.branch || authority.context.branch),
146
+ changeSlug: text(authority.context.change_slug),
147
+ };
148
+ for (const [snake, camel] of IDENTITY_FIELDS) {
149
+ const suppliedValues = suppliedIdentityValues(input, shared, snake, camel);
150
+ if (suppliedValues.some((supplied) => supplied !== expected[camel])) {
151
+ throw contextError(
152
+ 'WENDKEEP_HANDOFF_CONTEXT_MISMATCH',
153
+ `${snake} do handoff não corresponde ao active context causal`,
154
+ );
155
+ }
156
+ delete shared[camel];
157
+ if (expected[camel]) shared[snake] = expected[camel];
158
+ else delete shared[snake];
159
+ }
160
+ return Object.keys(shared).length ? shared : null;
161
+ }
162
+
163
+ export function causalChangeSlug(entry = {}, authority = {}) {
164
+ return authority?.mode === 'contextual'
165
+ ? text(authority.context?.change_slug)
166
+ : text(entry?.change_slug);
167
+ }
168
+
169
+ export function scopeEvidenceRows(rows, { activeContext = null, registry = {} } = {}) {
170
+ if (!Array.isArray(rows) || !activeContextRegistryInitialized(registry)) return rows;
171
+
172
+ const callerWorkSession = text(activeContext?.workSessionId ?? activeContext?.work_session_id);
173
+ const sessions = registry.sessions || {};
174
+ const activeContexts = Object.values(contextsOf(registry))
175
+ .filter((context) => context?.state === 'active');
176
+ const activeChangeOwners = new Map();
177
+ for (const context of activeContexts) {
178
+ const slug = text(context.change_slug);
179
+ if (slug) activeChangeOwners.set(slug, text(context.work_session_id));
180
+ }
181
+
182
+ return rows.filter((row) => {
183
+ const sessionId = text(row?.session_id);
184
+ const rowWorkSession = text(row?.work_session_id)
185
+ || text(sessionId && sessions[sessionId]?.work_session_id);
186
+ if (sessionId && !rowWorkSession) return false;
187
+ if (rowWorkSession && rowWorkSession !== callerWorkSession) return false;
188
+ if (rowWorkSession && !callerWorkSession) return false;
189
+
190
+ const changeSlug = text(row?.change_slug);
191
+ const owner = activeChangeOwners.get(changeSlug) || '';
192
+ if (owner && owner !== callerWorkSession) return false;
193
+ if (owner && !callerWorkSession) return false;
194
+ return true;
195
+ });
196
+ }
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  // UserPromptSubmit: bounded, read-only retrieval from the local evidence index.
3
3
  import { pathToFileURL } from 'node:url';
4
- import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
4
+ import { readHookInput, readSessionRegistry, writeHookOutput } from './obsidian-common.mjs';
5
5
  import { loadEvidenceIndex, recallEvidence, renderEvidenceContext } from './evidence-recall.mjs';
6
6
  import { sanitizeMemoryText } from './memory-schema.mjs';
7
7
  import { resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
8
+ import {
9
+ resolveReadOnlyEvidenceActiveContext,
10
+ scopeEvidenceRows,
11
+ } from './active-context-handoff-evidence.mjs';
8
12
  import { isBootstrapPrompt } from '../packages/integrations/src/prompt-content.mjs';
9
13
 
10
14
  export function buildPromptEvidenceContext(vaultBase, prompt, {
11
- topK = 3, maxBytes = 3072, rows = null,
15
+ topK = 3, maxBytes = 3072, rows = null, activeContext = null, registry = null,
12
16
  } = {}) {
13
17
  const query = sanitizeMemoryText(String(prompt || '')).trim();
14
18
  if (!query || isBootstrapPrompt(query)) return '';
15
19
  const evidence = rows || loadEvidenceIndex(vaultBase);
16
- if (!evidence.length) return '';
20
+ const scoped = scopeEvidenceRows(evidence, {
21
+ activeContext,
22
+ registry: registry || readSessionRegistry(vaultBase),
23
+ });
24
+ if (!scoped.length) return '';
17
25
  return sanitizeMemoryText(renderEvidenceContext(
18
- recallEvidence(evidence, query, { topK }),
26
+ recallEvidence(scoped, query, { topK }),
19
27
  { maxBytes },
20
28
  ));
21
29
  }
@@ -23,13 +31,20 @@ export function buildPromptEvidenceContext(vaultBase, prompt, {
23
31
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
24
32
  try {
25
33
  const input = readHookInput();
26
- const runtime = resolveHookOperatingProfile({ input });
34
+ const runtime = resolveHookOperatingProfile({
35
+ input,
36
+ activeContextResolver: resolveReadOnlyEvidenceActiveContext,
37
+ });
27
38
  if (runtime.bindingError) {
28
39
  writeHookOutput({});
29
40
  } else {
30
41
  const context = buildPromptEvidenceContext(
31
42
  runtime.vaultBase,
32
43
  input.prompt || input.user_prompt || '',
44
+ {
45
+ activeContext: runtime.activeContext,
46
+ registry: readSessionRegistry(runtime.vaultBase),
47
+ },
33
48
  );
34
49
  writeHookOutput(context ? {
35
50
  hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context },
@@ -133,6 +133,7 @@ export function resolveHookOperatingProfile({
133
133
  resolution = null,
134
134
  config = null,
135
135
  provider,
136
+ activeContextResolver = resolveCommandActiveContext,
136
137
  } = {}) {
137
138
  const initialResolution = resolution || resolveProjectVault({ input });
138
139
  const matched = matchingProjectBinding(initialResolution, input);
@@ -158,7 +159,7 @@ export function resolveHookOperatingProfile({
158
159
  let contextError = null;
159
160
  if (!bindingError && identity.state === 'resolved') {
160
161
  try {
161
- activeContext = resolveCommandActiveContext({
162
+ activeContext = activeContextResolver({
162
163
  vaultBase: vaultResolution.base,
163
164
  projectRoot: input?.cwd || vaultResolution.projectRoot || process.cwd(),
164
165
  sessionId: identity.canonicalConversationId,
@@ -19,6 +19,11 @@ import { mutateSessionNote } from './session-note-io.mjs';
19
19
  import { appendIterationOutcome } from './session-iteration-outcome.mjs';
20
20
  import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
21
21
  import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
22
+ import {
23
+ buildCausalSharedHandoff,
24
+ causalChangeSlug,
25
+ resolveHandoffEvidenceAuthority,
26
+ } from './active-context-handoff-evidence.mjs';
22
27
  import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
23
28
  import { detectMemoryMode } from './memory-mode.mjs';
24
29
  import { sanitizeMemoryText } from './memory-schema.mjs';
@@ -521,21 +526,6 @@ function shouldFinalizeSession() {
521
526
  return process.env.OBSIDIAN_NO_AUTO_FINALIZE !== '1';
522
527
  }
523
528
 
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
-
539
529
  export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
540
530
  if (detectMemoryMode(vaultBase).mode === 'legacy') {
541
531
  return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
@@ -1277,6 +1267,29 @@ export async function main({
1277
1267
  const tx = parseTranscript(identity.transcriptPath || input.transcript_path || input.transcriptPath);
1278
1268
  const requestedTurnId = String(input.turn_id || input.turnId || '');
1279
1269
  const sessionId = identity.canonicalConversationId;
1270
+ let handoffEvidenceAuthority;
1271
+ let sharedHandoff;
1272
+ let activeContextChangeSlug;
1273
+ try {
1274
+ handoffEvidenceAuthority = resolveHandoffEvidenceAuthority(vaultBase, {
1275
+ input,
1276
+ sessionId,
1277
+ projectRoot: input.cwd || process.cwd(),
1278
+ });
1279
+ sharedHandoff = buildCausalSharedHandoff({
1280
+ input,
1281
+ entry,
1282
+ authority: handoffEvidenceAuthority,
1283
+ });
1284
+ activeContextChangeSlug = causalChangeSlug(entry, handoffEvidenceAuthority);
1285
+ } catch (error) {
1286
+ const detail = String(error?.message || 'active context causal indisponível');
1287
+ process.stderr.write(`[wendkeep] Stop causal bloqueado: ${detail}\n`);
1288
+ writeHookOutput({
1289
+ systemMessage: `wendkeep: Stop bloqueado antes de publicar handoff/evidência: ${detail}`,
1290
+ });
1291
+ return;
1292
+ }
1280
1293
  const finalizing = shouldFinalizeSession();
1281
1294
  const turnIdentity = resolveTurnIdentity(tx, requestedTurnId);
1282
1295
  if (!turnIdentity) {
@@ -1389,11 +1402,10 @@ export async function main({
1389
1402
  }
1390
1403
  const finalSummary = sessionFinalSummary(tx);
1391
1404
  const memoryEvidence = collectLifecycleEvidence(vaultBase, {
1392
- changeSlug: entry.change_slug,
1405
+ changeSlug: activeContextChangeSlug,
1393
1406
  summary: finalSummary,
1394
1407
  noteRel: sessionRel,
1395
1408
  });
1396
- const sharedHandoff = sharedHandoffFromInput(input, entry);
1397
1409
  memoryHandoff = {
1398
1410
  projectId,
1399
1411
  identity,
@@ -1531,9 +1543,11 @@ export async function main({
1531
1543
  // grafo quando a change fechava antes do turno seguinte. Aqui sobrevive ao reopen e acumula toda
1532
1544
  // change que passou pela sessão (upsertListSection deduplica). Fail-quiet: nunca derruba o Stop.
1533
1545
  try {
1534
- const chgLink = entry.change_slug
1535
- ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${entry.change_slug}/proposta]]`
1536
- : activeChangeLink(vaultBase);
1546
+ const chgLink = activeContextChangeSlug
1547
+ ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${activeContextChangeSlug}/proposta]]`
1548
+ : handoffEvidenceAuthority.mode === 'legacy'
1549
+ ? activeChangeLink(vaultBase)
1550
+ : '';
1537
1551
  const wl = (chgLink.match(/\[\[[^\]]+\]\]/) || [])[0];
1538
1552
  if (wl) {
1539
1553
  mutateSessionNote(sessionPath, (cur) => (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.76.6",
3
+ "version": "0.76.7",
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": [
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
45
- "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.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-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-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 src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.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-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.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",
45
+ "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.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-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-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 src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.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-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.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",
46
46
  "test": "node --test --test-concurrency=2",
47
47
  "test:core": "node scripts/run-scope.mjs core",
48
48
  "release": "node scripts/release.mjs",