wendkeep 0.76.8 → 0.76.9

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,20 @@ 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.9] — 2026-08-22
8
+
9
+ ### Fixed
10
+
11
+ - **Injeção de change causal.** `brain-inject` e `change-context` usam o active context da sessão
12
+ para marcar `ATUAL`, calcular o hash da sentinela e preservar o mesmo foco em todos os degraus do
13
+ budget; o backlog continua global sem promover a change irmã pelo pointer legado.
14
+ - **Store vazio fail-closed.** A presença de `active_contexts`, schema ou revision — inclusive
15
+ `active_contexts: {}` — impede fallback a `CURRENT_CHANGE.md` quando o contexto causal está ausente
16
+ ou ambíguo; compatibilidade legada permanece somente antes da inicialização contextual.
17
+ - **Criação contextual preservada.** Operações mutáveis como `delivery start` continuam recebendo a
18
+ identidade causal antes de existir um context record, enquanto hooks read-only exigem o registro
19
+ existente e nunca publicam foco de sibling.
20
+
7
21
  ## [0.76.8] — 2026-08-22
8
22
 
9
23
  ### Added
package/README.en.md CHANGED
@@ -116,6 +116,7 @@ npx wendkeep init
116
116
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
117
117
  2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
118
118
  3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. `doctor` diagnoses orphaned active contexts, removed worktrees, and expired `request-stop` leases without writing; `context repair --key <key> --revision <n> --reason <text> --session <id>` revalidates under lock, closes only the ownerless/removed context or expires only its lease, while preserving the record and all historical memory. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
+ Once `active_contexts` is initialized, `brain-inject` and `change-context` mark only the causal context's change as current; the backlog remains global, and an empty or ambiguous store never revives `CURRENT_CHANGE.md`.
119
120
  4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
120
121
  5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
121
122
  - **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
@@ -582,7 +583,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
582
583
  (Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
583
584
  ```
584
585
 
585
- The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
586
+ The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the causal current-change marker, project lessons, and a `<wk_process>` router. The inventory remains global, but `CURRENT` and the sentinel hash come from the session's active context; `active_contexts: {}` fails closed instead of reviving `CURRENT_CHANGE.md`. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
586
587
 
587
588
  The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
588
589
 
package/README.md CHANGED
@@ -116,6 +116,7 @@ npx wendkeep init
116
116
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
117
117
  2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
118
118
  3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. `doctor` diagnoses orphaned active contexts, removed worktrees, and expired `request-stop` leases without writing; `context repair --key <key> --revision <n> --reason <text> --session <id>` revalidates under lock, closes only the ownerless/removed context or expires only its lease, while preserving the record and all historical memory. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
+ Once `active_contexts` is initialized, `brain-inject` and `change-context` mark only the causal context's change as current; the backlog remains global, and an empty or ambiguous store never revives `CURRENT_CHANGE.md`.
119
120
  4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
120
121
  5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
121
122
  - **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
@@ -582,7 +583,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
582
583
  (Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
583
584
  ```
584
585
 
585
- The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the global current-change marker, project lessons, and a `<wk_process>` router. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
586
+ The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the causal current-change marker, project lessons, and a `<wk_process>` router. The inventory remains global, but `CURRENT` and the sentinel hash come from the session's active context; `active_contexts: {}` fails closed instead of reviving `CURRENT_CHANGE.md`. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
586
587
 
587
588
  The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
588
589
 
@@ -117,6 +117,14 @@ unambiguous active context. With zero or multiple contexts it stays empty. Migra
117
117
  and never invents a worktree or session identity. The legacy pointer becomes a context only
118
118
  when one active session, a complete scope, and worktree metadata prove one identity.
119
119
 
120
+ The `brain-inject` (`SessionStart`) and `change-context` (`UserPromptSubmit`) hooks resolve the same
121
+ causal identity before marking a change as `CURRENT` or computing the sentinel hash. The backlog
122
+ remains global and lists every other change as `OPEN`, but a divergent `CURRENT_CHANGE.md` never
123
+ turns a sibling change into the session focus. The presence of `active_contexts`, its schema, or its
124
+ revision — including `active_contexts: {}` — disables legacy fallback: a missing or ambiguous
125
+ context fails closed without reviving the pointer. Fallback exists only before contextual-store
126
+ initialization.
127
+
120
128
  ## Common errors and diagnosis
121
129
 
122
130
  - `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
@@ -114,6 +114,13 @@ contexto ativo inequívoco. Com zero ou múltiplos contextos, fica vazio. A migr
114
114
  não inventa uma identidade de worktree ou sessão. O ponteiro legado só vira contexto quando uma sessão
115
115
  ativa, scope completa e metadados da worktree provam uma única identidade.
116
116
 
117
+ Os hooks `brain-inject` (`SessionStart`) e `change-context` (`UserPromptSubmit`) resolvem a mesma
118
+ identidade causal antes de marcar uma change como `ATUAL` ou calcular o hash da sentinela. O backlog
119
+ continua global e lista as demais changes como `ABERTA`, mas um `CURRENT_CHANGE.md` divergente nunca
120
+ transforma a change irmã em foco da sessão. A presença de `active_contexts`, schema ou revision —
121
+ inclusive `active_contexts: {}` — desativa o fallback legado: contexto ausente ou ambíguo falha
122
+ fechado sem reativar o ponteiro. O fallback só existe antes da inicialização do store contextual.
123
+
117
124
  ## Erros comuns e diagnóstico
118
125
 
119
126
  - `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
@@ -1,7 +1,12 @@
1
1
  import { readSessionRegistry } from './obsidian-common.mjs';
2
- import { resolveActiveContext } from './active-context-store.mjs';
2
+ import {
3
+ activeContextRegistryInitialized,
4
+ resolveActiveContext,
5
+ } from './active-context-store.mjs';
3
6
  import { resolveRuntimeActiveContext } from '../src/active-context-runtime.mjs';
4
7
 
8
+ export { activeContextRegistryInitialized } from './active-context-store.mjs';
9
+
5
10
  const IDENTITY_FIELDS = Object.freeze([
6
11
  ['work_session_id', 'workSessionId'],
7
12
  ['repository_id', 'repositoryId'],
@@ -26,12 +31,6 @@ function contextsOf(registry) {
26
31
  ? registry.active_contexts : {};
27
32
  }
28
33
 
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
34
  function assertAuthority(identity, context) {
36
35
  if (!identity || !context || context.state !== 'active') {
37
36
  throw contextError(
@@ -11,6 +11,12 @@ const ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
11
11
  const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
12
12
  const DELIVERY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,100}$/;
13
13
 
14
+ export function activeContextRegistryInitialized(registry = {}) {
15
+ return Object.hasOwn(registry, 'active_contexts')
16
+ || Object.hasOwn(registry, 'active_contexts_schema')
17
+ || Object.hasOwn(registry, 'active_contexts_revision');
18
+ }
19
+
14
20
  function contextError(code, message) {
15
21
  const error = new Error(message);
16
22
  error.code = code;
@@ -254,7 +260,7 @@ export function migrateLegacyActiveContext(vaultBase, {
254
260
  const slug = legacyPointer(vaultBase);
255
261
  if (!slug) return { migrated: false, reason: 'no-pointer' };
256
262
  const registry = readSessionRegistry(vaultBase);
257
- if (Object.keys(contextsOf(registry)).length) return { migrated: false, reason: 'already-initialized' };
263
+ if (activeContextRegistryInitialized(registry)) return { migrated: false, reason: 'already-initialized' };
258
264
  if (typeof identityForSession !== 'function') return { migrated: false, reason: 'identity-unavailable' };
259
265
 
260
266
  const candidates = [];
@@ -7,7 +7,12 @@ import { join } from 'node:path';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
9
9
  import { brainDir } from './brain-core.mjs';
10
- import { buildActiveChangeInjection, changeCtxState, writeSentinel } from './change-core.mjs';
10
+ import {
11
+ activeChange,
12
+ buildActiveChangeInjection,
13
+ changeCtxState,
14
+ writeSentinel,
15
+ } from './change-core.mjs';
11
16
  import { buildLessonsInjection } from './lessons-core.mjs';
12
17
  import { getLocale } from './locale.mjs';
13
18
  import { resolveSessionEntry } from './session-identity.mjs';
@@ -20,6 +25,7 @@ import {
20
25
  } from './operating-profile-runtime.mjs';
21
26
  import { assertVaultPathSafe } from './vault-path-safety.mjs';
22
27
  import { CORE_LIMITS, validateCore } from '../src/validate-core.mjs';
28
+ import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
23
29
 
24
30
  // The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
25
31
  // standing instruction the model plans in chat, leaves the change scaffold raw and forces the
@@ -220,7 +226,9 @@ function budgetNotice(priority, layer, message) {
220
226
  return `<wk_budget_notice priority="${priority}" layer="${layer}">${message}</wk_budget_notice>`;
221
227
  }
222
228
 
223
- export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bindingError = null } = {}) {
229
+ export function buildInjection(vaultBase, input = {}, {
230
+ profile = 'GOVERN', bindingError = null, context = null,
231
+ } = {}) {
224
232
  const brain = detectMemoryMode(vaultBase).mode === 'v2'
225
233
  ? buildV2Memory(vaultBase)
226
234
  : buildLegacyMemory(vaultBase);
@@ -236,10 +244,14 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
236
244
  // visible below, but the standing change router would contradict that contract.
237
245
  const router = policy.requiresChange ? processRouter(getLocale(vaultBase).id) : '';
238
246
  const { identity, entry } = resolveSessionEntry(vaultBase, input);
239
- const focus = identity.state === 'resolved' && entry?.change_slug
240
- ? `<session_change>${boundAncillaryText(`Change vinculada a esta sessão: ${entry.change_slug}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.`, '<session_change></session_change>'.length)}</session_change>`
247
+ const contextualChange = context ? activeChange(vaultBase, { context }) : '';
248
+ const sessionChange = contextualChange || (!context && identity.state === 'resolved' ? entry?.change_slug : '');
249
+ const focus = sessionChange
250
+ ? `<session_change>${boundAncillaryText(`Change vinculada a esta sessão: ${sessionChange}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.`, '<session_change></session_change>'.length)}</session_change>`
241
251
  : '';
242
- const allChanges = buildActiveChangeInjection(vaultBase, { maxLineChars: INJECTION_LIMITS.lineChars });
252
+ const allChanges = buildActiveChangeInjection(vaultBase, {
253
+ context, maxLineChars: INJECTION_LIMITS.lineChars,
254
+ });
243
255
 
244
256
  // Global priority is deterministic: memory/router/focus, then changes, then lessons.
245
257
  let output = joinInjection([brain, profileNotice, router, focus, allChanges, lessons]);
@@ -253,6 +265,7 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
253
265
  // Second pressure step: non-current changes leave the hot context before the current one.
254
266
  const nonCurrentEvicted = budgetNotice(2, 'non-current-changes', 'Changes não atuais omitidas depois das lessons.');
255
267
  const currentChange = buildActiveChangeInjection(vaultBase, {
268
+ context,
256
269
  currentOnly: true,
257
270
  maxLineChars: INJECTION_LIMITS.lineChars,
258
271
  });
@@ -264,6 +277,7 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
264
277
  const fixed = joinInjection([brain, profileNotice, router, focus, lessonsEvicted, nonCurrentEvicted, currentSummarized]);
265
278
  const remaining = Math.max(512, INJECTION_LIMITS.totalBytes - byteLength(fixed) - 1);
266
279
  const boundedCurrent = buildActiveChangeInjection(vaultBase, {
280
+ context,
267
281
  currentOnly: true,
268
282
  maxBytes: remaining,
269
283
  maxLineChars: INJECTION_LIMITS.lineChars,
@@ -285,21 +299,29 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
285
299
  const input = readHookInput();
286
300
  const runtime = resolveHookOperatingProfile({ input });
287
301
  const vaultBase = runtime.vaultBase;
302
+ const { identity } = runtime;
303
+ const sid = identity.state === 'resolved'
304
+ ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
305
+ const commandContext = runtime.bindingError ? null : resolveCommandActiveContext({
306
+ vaultBase,
307
+ projectRoot: input.cwd || process.cwd(),
308
+ sessionId: sid,
309
+ requireExisting: true,
310
+ });
288
311
  writeHookOutput({
289
312
  hookSpecificOutput: {
290
313
  hookEventName: 'SessionStart',
291
314
  additionalContext: buildInjection(vaultBase, input, {
292
315
  profile: runtime.profile,
293
316
  bindingError: runtime.bindingError,
317
+ context: commandContext,
294
318
  }),
295
319
  },
296
320
  });
297
321
  // Sentinela do change-context: o backlog completo acabou de ser injetado aqui, então o hook
298
322
  // UserPromptSubmit não precisa re-pingar no 1º prompt. Bônus — nunca derruba a injeção.
299
323
  if (runtime.policy.harness && !runtime.bindingError) try {
300
- const st = changeCtxState(vaultBase);
301
- const { identity } = runtime;
302
- const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
324
+ const st = changeCtxState(vaultBase, { context: commandContext });
303
325
  if (st) {
304
326
  writeSentinel(vaultBase, 'ctx', profileSentinelId(sid, runtime.profile), st.hash);
305
327
  }
@@ -8,7 +8,9 @@
8
8
  import { pathToFileURL } from 'node:url';
9
9
  import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
10
10
  import { profileRuntimeError } from './brain-inject.mjs';
11
- import { changeCtxState, readSentinel, renderOpenChanges, writeSentinel } from './change-core.mjs';
11
+ import {
12
+ activeChange, changeCtxState, readSentinel, renderOpenChanges, writeSentinel,
13
+ } from './change-core.mjs';
12
14
  import {
13
15
  hookProfilePolicy,
14
16
  profileSentinelId,
@@ -43,7 +45,7 @@ export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug =
43
45
  hash,
44
46
  };
45
47
  }
46
- const st = changeCtxState(vaultBase);
48
+ const st = changeCtxState(vaultBase, { context });
47
49
  if (st) {
48
50
  if (readSentinel(vaultBase, 'ctx', sentinelId) === st.hash) return null;
49
51
  writeSentinel(vaultBase, 'ctx', sentinelId, st.hash);
@@ -75,10 +77,14 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
75
77
  vaultBase,
76
78
  projectRoot: input.cwd || process.cwd(),
77
79
  sessionId: sid,
80
+ requireExisting: true,
78
81
  });
82
+ const changeSlug = commandContext
83
+ ? activeChange(vaultBase, { context: commandContext })
84
+ : (entry?.change_slug || '');
79
85
  const ping = runtime.bindingError
80
86
  ? { context: profileRuntimeError(runtime.bindingError) }
81
- : buildChangePing(vaultBase, sid, input.prompt || '', entry?.change_slug || '', {
87
+ : buildChangePing(vaultBase, sid, input.prompt || '', changeSlug, {
82
88
  profile: runtime.profile,
83
89
  context: commandContext,
84
90
  });
@@ -420,7 +420,8 @@ export function renderOpenChanges(state, {
420
420
  // Mantém o nome exportado para consumidores internos existentes, mas agora injeta o backlog
421
421
  // completo em vez de ocultar changes não selecionadas.
422
422
  export function buildActiveChangeInjection(vaultBase, options = {}) {
423
- return renderOpenChanges(allChangesState(vaultBase), options);
423
+ const { context, ...renderOptions } = options;
424
+ return renderOpenChanges(allChangesState(vaultBase, { context }), renderOptions);
424
425
  }
425
426
 
426
427
  export function activeChangeLink(vaultBase, { context } = {}) {
@@ -479,8 +480,8 @@ export function writeSentinel(vaultBase, kind, sid, value = '1') {
479
480
  // Estado global usado pelo change-context: hash cobre qualquer tarefa aberta, inclusive de uma
480
481
  // change que não esteja no ponteiro. As propriedades slug/openTasks preservam compatibilidade com
481
482
  // consumidores antigos e descrevem somente a atual.
482
- export function changeCtxState(vaultBase) {
483
- const state = allChangesState(vaultBase);
483
+ export function changeCtxState(vaultBase, { context } = {}) {
484
+ const state = allChangesState(vaultBase, { context });
484
485
  if (!state.changes.length && !state.pointerWarning) return null;
485
486
  const selected = state.changes.find((change) => change.current);
486
487
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.76.8",
3
+ "version": "0.76.9",
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": [
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check packages/vault/src/worktree-metadata.mjs",
44
+ "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
45
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/active-context-health.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",
@@ -1,5 +1,9 @@
1
1
  import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
2
- import { migrateLegacyActiveContext } from '../hooks/active-context-store.mjs';
2
+ import {
3
+ activeContextRegistryInitialized,
4
+ migrateLegacyActiveContext,
5
+ resolveActiveContext,
6
+ } from '../hooks/active-context-store.mjs';
3
7
  import { captureProjectScope, compareProjectScopes } from '../hooks/project-scope.mjs';
4
8
  import {
5
9
  discoverWorktreeRepository,
@@ -107,6 +111,7 @@ export function resolveCommandActiveContext({
107
111
  projectRoot = process.cwd(),
108
112
  sessionId = '',
109
113
  spawn,
114
+ requireExisting = false,
110
115
  } = {}) {
111
116
  const requestedSession = String(sessionId || '').trim();
112
117
  let identity;
@@ -116,7 +121,7 @@ export function resolveCommandActiveContext({
116
121
  });
117
122
  } catch (error) {
118
123
  const registry = readSessionRegistry(vaultBase);
119
- const initialized = Object.keys(registry.active_contexts || {}).length > 0;
124
+ const initialized = activeContextRegistryInitialized(registry);
120
125
  const legacyUnavailable = error?.code === 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE'
121
126
  || (!requestedSession && error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND');
122
127
  if (!initialized && legacyUnavailable) return null;
@@ -127,5 +132,10 @@ export function resolveCommandActiveContext({
127
132
  vaultBase, projectRoot, sessionId: candidateId, ...(spawn ? { spawn } : {}),
128
133
  }),
129
134
  });
135
+ if (requireExisting) {
136
+ const registry = readSessionRegistry(vaultBase);
137
+ if (!activeContextRegistryInitialized(registry)) return null;
138
+ resolveActiveContext(vaultBase, identity);
139
+ }
130
140
  return identity;
131
141
  }