wendkeep 0.80.0 → 0.80.1

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,17 @@ 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.80.1] — 2026-08-24
8
+
9
+ ### Fixed
10
+
11
+ - **Bootstrap causal das sessões Codex.** `session-start` e `session-ensure` agora derivam um
12
+ `work_session_id` estável do `session_id` canônico quando o host não envia essa identidade,
13
+ preservando primeiro handoff explícito e valor já registrado. Novas sessões deixam de falhar em
14
+ `profile route`, `change new` e active context por identidade ausente, sem relaxar as validações
15
+ de projeto, repositório, worktree ou branch; README e o guia de contexto foram atualizados em
16
+ PT-BR/EN.
17
+
7
18
  ## [0.80.0] — 2026-08-24
8
19
 
9
20
  ### Added
package/README.en.md CHANGED
@@ -115,7 +115,7 @@ npx wendkeep init
115
115
 
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
- 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**.
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`. When the host omits `work_session_id`, `session-start` and `session-ensure` derive it from the canonical `session_id`, preserving an explicit handoff and the already registered value first. 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
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`.
120
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`.)
121
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:
package/README.md CHANGED
@@ -115,7 +115,7 @@ npx wendkeep init
115
115
 
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
- 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**.
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`. When the host omits `work_session_id`, `session-start` and `session-ensure` derive it from the canonical `session_id`, preserving an explicit handoff and the already registered value first. 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
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`.
120
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`.)
121
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:
@@ -108,6 +108,11 @@ identified by `repository_id` + `worktree_id` + `work_session_id`; branch, HEAD,
108
108
  belong to that entry. Two worktrees can therefore select different changes without overwriting
109
109
  each other's operational focus.
110
110
 
111
+ During the session lifecycle, `session-start` and `session-ensure` first preserve an explicit
112
+ handoff `work_session_id`, then the already registered value, and, when the host supplies neither,
113
+ derive the stable identity from the canonical `session_id`. Providers such as Codex can therefore
114
+ initialize an active context without weakening project, repository, worktree, or branch checks.
115
+
111
116
  With an explicit causal session, change, spec, and verify resolve only the matching entry. Without
112
117
  a session, only one active entry for the worktree is accepted; two sessions produce ambiguity and
113
118
  the operation must fail closed without silently selecting a change.
@@ -105,6 +105,12 @@ identificada por `repository_id` + `worktree_id` + `work_session_id`; branch, HE
105
105
  pertencem a essa entrada. Assim, duas worktrees podem selecionar changes diferentes sem
106
106
  sobrescrever o foco uma da outra.
107
107
 
108
+ No lifecycle de sessão, `session-start` e `session-ensure` preservam primeiro um
109
+ `work_session_id` explícito do handoff, depois o valor já registrado e, quando o host não fornece
110
+ nenhum deles, derivam a identidade estável do `session_id` canônico. Assim, providers como o Codex
111
+ podem inicializar o active context sem enfraquecer as validações de projeto, repositório, worktree
112
+ ou branch.
113
+
108
114
  Com sessão causal explícita, change, spec e verify resolvem somente a entrada correspondente. Sem
109
115
  sessão, uma única entrada ativa da worktree pode ser usada; duas sessões compatíveis causam
110
116
  ambiguidade e a operação falha fechada, sem escolher uma change em silêncio.
@@ -32,31 +32,19 @@ import {
32
32
  writeHookOutput,
33
33
  yamlQuote,
34
34
  } from './obsidian-common.mjs';
35
- import { resolveSessionIdentity } from './session-identity.mjs';
35
+ import { resolveSessionIdentity, sessionWorkSessionPatch } 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';
40
39
 
41
40
  function sessionIdFromInput(input) {
42
41
  return input.session_id || input.sessionId || input.codex_session_id || '';
43
42
  }
44
43
 
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 } : {};
44
+ function workSessionPatch(vaultBase, sessionId, input = {}) {
45
+ const existingWorkSessionId = readSessionRegistry(vaultBase)
46
+ .sessions?.[sessionId]?.work_session_id || '';
47
+ return sessionWorkSessionPatch({ input, sessionId, existingWorkSessionId });
60
48
  }
61
49
 
62
50
  function turnSequenceFromInput(input = {}) {
@@ -308,7 +296,7 @@ function activateExistingSession({ vaultBase, relPath, startedAt, sessionId, inp
308
296
  transcript_path: identity.transcriptPath,
309
297
  transcript_id: identity.transcriptId,
310
298
  provider: identity.provider,
311
- ...workSessionPatch(input),
299
+ ...workSessionPatch(vaultBase, sessionId, input),
312
300
  ...scopePatch,
313
301
  ...causalTurnPatch(input, now),
314
302
  });
@@ -337,7 +325,7 @@ function createSession({ vaultBase, sessionId, input, now, identity, scopePatch
337
325
  transcript_path: identity.transcriptPath,
338
326
  transcript_id: identity.transcriptId,
339
327
  provider: identity.provider,
340
- ...workSessionPatch(input),
328
+ ...workSessionPatch(vaultBase, sessionId, input),
341
329
  ...scopePatch,
342
330
  ...causalTurnPatch(input, now),
343
331
  });
@@ -381,7 +369,7 @@ function main() {
381
369
  upsertSessionRegistry(vaultBase, sessionId, {
382
370
  transcript_paths: [identity.transcriptPath],
383
371
  provider: identity.provider,
384
- ...workSessionPatch(input),
372
+ ...workSessionPatch(vaultBase, sessionId, input),
385
373
  });
386
374
  }
387
375
  writeHookOutput({});
@@ -419,7 +407,7 @@ function main() {
419
407
  transcript_path: identity.transcriptPath,
420
408
  transcript_id: identity.transcriptId,
421
409
  provider: identity.provider,
422
- ...workSessionPatch(input),
410
+ ...workSessionPatch(vaultBase, sessionId, input),
423
411
  ...scopePatch,
424
412
  ...causalTurnPatch(input, now),
425
413
  });
@@ -465,7 +453,7 @@ function main() {
465
453
  transcript_path: identity.transcriptPath,
466
454
  transcript_id: identity.transcriptId,
467
455
  provider: identity.provider,
468
- ...workSessionPatch(input),
456
+ ...workSessionPatch(vaultBase, sessionId || control.session_id, input),
469
457
  ...scopePatch,
470
458
  ...causalTurnPatch(input, now),
471
459
  });
@@ -6,6 +6,26 @@ import {
6
6
  resolveSessionIdentitySnapshot,
7
7
  } from '../packages/integrations/src/session-identity.mjs';
8
8
  import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
9
+ import { sanitizeMemoryText } from './memory-schema.mjs';
10
+
11
+ export function sessionWorkSessionPatch({
12
+ input = {},
13
+ sessionId = '',
14
+ existingWorkSessionId = '',
15
+ } = {}) {
16
+ const shared = input.shared || input.handoff?.shared;
17
+ const explicit = input.work_session_id
18
+ || input.workSessionId
19
+ || shared?.work_session_id
20
+ || shared?.workSessionId
21
+ || input.handoff?.work_session_id
22
+ || input.handoff?.workSessionId
23
+ || '';
24
+ const workSessionId = sanitizeMemoryText(
25
+ explicit || existingWorkSessionId || sessionId,
26
+ ).trim();
27
+ return workSessionId ? { work_session_id: workSessionId } : {};
28
+ }
9
29
 
10
30
  function unknownTranscriptIdentity() {
11
31
  return {
@@ -32,7 +32,7 @@ import {
32
32
  writeHookOutput,
33
33
  yamlQuote,
34
34
  } from './obsidian-common.mjs';
35
- import { resolveSessionIdentity } from './session-identity.mjs';
35
+ import { resolveSessionIdentity, sessionWorkSessionPatch } from './session-identity.mjs';
36
36
  import { captureProjectScope, projectScopePatch } from './project-scope.mjs';
37
37
 
38
38
  export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
@@ -183,11 +183,18 @@ function main() {
183
183
  readSessionRegistry(vaultBase).sessions?.[canonicalSessionId]?.project_scope,
184
184
  { ...initialScope, sessionId: canonicalSessionId },
185
185
  );
186
+ const workSessionPatchFor = (canonicalSessionId) => sessionWorkSessionPatch({
187
+ input,
188
+ sessionId: canonicalSessionId,
189
+ existingWorkSessionId: readSessionRegistry(vaultBase)
190
+ .sessions?.[canonicalSessionId]?.work_session_id || '',
191
+ });
186
192
  const control = readControl(vaultBase);
187
193
  const activationId = input.activation_id || input.activationId || randomUUID();
188
194
  const activationStartedAt = formatLocalIso(now);
189
195
  const registerActivation = (canonicalSessionId, patch) => upsertSessionRegistry(vaultBase, canonicalSessionId, {
190
196
  ...patch,
197
+ ...workSessionPatchFor(canonicalSessionId),
191
198
  ...scopePatchFor(canonicalSessionId),
192
199
  activation_id: activationId,
193
200
  activation_started_at: activationStartedAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.80.0",
3
+ "version": "0.80.1",
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": [