wendkeep 0.76.0 → 0.76.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,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.1] — 2026-08-21
8
+
9
+ ### Added
10
+
11
+ - **Transição causal de branch.** `context switch <branch> [--create] [--session <id>]` move
12
+ Git e a scope da mesma sessão juntos na worktree atual, preserva change/lease/autorizações,
13
+ incrementa revisão e registra evidência `from/to` sob o lock do registry.
14
+
15
+ ### Fixed
16
+
17
+ - **Guard não deixa mais a sessão inutilizável após checkout.** `git checkout/switch` cru que
18
+ mudaria branch é negado antecipadamente com orientação acionável; falha durante a transição
19
+ segura executa rollback da branch/HEAD e não publica scope parcial. Contextos incompletos de
20
+ outra worktree comprovada também deixam de causar falso conflito na worktree atual.
21
+
7
22
  ## [0.76.0] — 2026-08-21
8
23
 
9
24
  ### 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. 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`. 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. 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
  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
120
  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
121
  - **`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`.
@@ -239,6 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
239
239
  |---|---|---|
240
240
  | **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
241
241
  | **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
242
+ | **Active context** | `context switch`, causal branch transition, and rollback | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
242
243
  | **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
243
244
  | **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
244
245
  | **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
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. 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`. 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. 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
  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
120
  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
121
  - **`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`.
@@ -239,6 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
239
239
  |---|---|---|
240
240
  | **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
241
241
  | **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
242
+ | **Active context** | `context switch`, causal branch transition, and rollback | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
242
243
  | **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
243
244
  | **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
244
245
  | **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
@@ -0,0 +1,88 @@
1
+ # Active context
2
+
3
+ **English** · [Português](../../pt-BR/commands/context.md)
4
+
5
+ ## Purpose
6
+
7
+ Switch the Git branch and the same session's causal scope together inside the current worktree,
8
+ without opening a new session or weakening the guard.
9
+
10
+ ## When to use
11
+
12
+ Use `context switch` when an active session must create or select another branch in the same
13
+ worktree and continue mutating the repository after the transition.
14
+
15
+ ## When not to use
16
+
17
+ Do not use it to move to another worktree, adopt an already-divergent scope, repair the registry,
18
+ or replace `worktree create`. Those cases require a separate physical context or explicit diagnosis.
19
+
20
+ ## Prerequisites
21
+
22
+ - A Git project bound to a Vault through `.wendkeep.json`.
23
+ - An active session with a complete `project_scope` matching the current worktree.
24
+ - Git on `PATH` and a branch accepted by `git check-ref-format --branch`.
25
+
26
+ ## Syntax
27
+
28
+ ```bash
29
+ npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <root>] [--vault <vault>] [--json]
30
+ ```
31
+
32
+ Without `--session`, exactly one active session must fully match the current scope. `--create`
33
+ uses `git switch -c`; without it, the command follows `git switch` semantics.
34
+
35
+ ## Options and exit codes
36
+
37
+ - `--create`: create the branch from the current HEAD.
38
+ - `--session <id>`: select the causal session explicitly; recommended whenever selection is unclear.
39
+ - `--project <root>` and `--vault <vault>`: select the binding and paths for manual use.
40
+ - `--json`: emit status, session id, branch, HEAD, revision, and event without exposing the Vault.
41
+
42
+ Exit `0` means the transition completed or the target was already active. Invalid usage,
43
+ ambiguity, scope mismatch, conflict, Git failure, or rollback returns `2` with a
44
+ `WENDKEEP_CONTEXT_*` code.
45
+
46
+ ## Examples
47
+
48
+ ```bash
49
+ npx --no-install wendkeep context switch wk/auth --create
50
+ npx --no-install wendkeep context switch main --session 019abc-session-id
51
+ npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
52
+ ```
53
+
54
+ Do not replace it with the raw command below while the harness is active:
55
+
56
+ ```bash
57
+ git switch -c wk/auth
58
+ ```
59
+
60
+ The guard returns `WENDKEEP_CONTEXT_SWITCH_REQUIRED` before Git runs, preventing the next mutation
61
+ from failing with a scope mismatch.
62
+
63
+ ## Expected result
64
+
65
+ The command validates the initial scope under lock, switches branch, proves project, repository,
66
+ remote, worktree, provider, and session id stayed unchanged, increments `context_revision`, and
67
+ appends a `from/to` event to `context_transitions`. The active change, task lease, and existing
68
+ authorizations are preserved.
69
+
70
+ If validation or persistence fails after the switch, rollback restores the previous branch or
71
+ detached HEAD; a branch created by the failed attempt is removed as well.
72
+
73
+ ## Common errors and diagnosis
74
+
75
+ - `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
76
+ - `WENDKEEP_CONTEXT_SCOPE_MISMATCH` or `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: return to the reserved
77
+ checkout or diagnose the session; the command never adopts a post-hoc divergence.
78
+ - `WENDKEEP_CONTEXT_CONFLICT`: another active context occupies the target; use another
79
+ branch/worktree or close the competing context correctly.
80
+ - `WENDKEEP_CONTEXT_GIT`: fix the branch, conflicting dirty state, or Git error and retry.
81
+ - `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git and registry state and diagnose manually before
82
+ any new mutation.
83
+ - `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: replace the raw Git command with `wendkeep context switch`.
84
+
85
+ ## Next steps
86
+
87
+ See [managed worktrees](worktrees.md) to create isolated checkouts and
88
+ [changes and verification](changes-and-verification.md) to continue the lifecycle on the new branch.
@@ -0,0 +1,86 @@
1
+ # Contexto ativo
2
+
3
+ **PT-BR** · [English](../../en/commands/context.md)
4
+
5
+ ## Objetivo
6
+
7
+ Trocar a branch Git e a scope causal da mesma sessão juntas, dentro da worktree atual, sem abrir
8
+ uma nova sessão e sem relaxar o guard.
9
+
10
+ ## Quando usar
11
+
12
+ Use `context switch` quando uma sessão ativa precisa criar ou selecionar outra branch na mesma
13
+ worktree e deve continuar mutando o repositório depois da troca.
14
+
15
+ ## Quando não usar
16
+
17
+ Não use para mudar de worktree, adotar uma scope já divergente, reparar o registry ou substituir
18
+ `worktree create`. Esses casos exigem outro contexto físico ou diagnóstico explícito.
19
+
20
+ ## Pré-requisitos
21
+
22
+ - Projeto Git vinculado a um Vault por `.wendkeep.json`.
23
+ - Sessão ativa com `project_scope` completa e correspondente à worktree atual.
24
+ - Git disponível no `PATH` e branch válida segundo `git check-ref-format --branch`.
25
+
26
+ ## Sintaxe
27
+
28
+ ```bash
29
+ npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <raiz>] [--vault <cofre>] [--json]
30
+ ```
31
+
32
+ Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à scope atual. Use
33
+ `--create` para executar `git switch -c`; sem a flag, a semântica é `git switch`.
34
+
35
+ ## Opções e códigos de saída
36
+
37
+ - `--create`: cria a branch a partir do HEAD atual.
38
+ - `--session <id>`: seleciona explicitamente a sessão causal; recomendado quando houver dúvida.
39
+ - `--project <raiz>` e `--vault <cofre>`: selecionam binding e paths para uso manual.
40
+ - `--json`: emite status, session id, branch, HEAD, revisão e evento sem expor o Vault.
41
+
42
+ Exit `0` significa transição concluída ou destino já ativo. Uso inválido, ambiguidade,
43
+ scope divergente, conflito, falha Git ou rollback retorna `2` com um código `WENDKEEP_CONTEXT_*`.
44
+
45
+ ## Exemplos
46
+
47
+ ```bash
48
+ npx --no-install wendkeep context switch wk/auth --create
49
+ npx --no-install wendkeep context switch main --session 019abc-session-id
50
+ npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
51
+ ```
52
+
53
+ Não substitua pelo comando cru abaixo quando o harness estiver ativo:
54
+
55
+ ```bash
56
+ git switch -c wk/auth
57
+ ```
58
+
59
+ O guard responde `WENDKEEP_CONTEXT_SWITCH_REQUIRED` antes do Git, evitando que a próxima mutação
60
+ falhe por mismatch.
61
+
62
+ ## Resultado esperado
63
+
64
+ O comando valida a scope inicial sob lock, troca a branch, prova que projeto, repositório, remoto,
65
+ worktree, provider e session id não mudaram, incrementa `context_revision` e anexa um evento
66
+ `from/to` em `context_transitions`. Change ativa, task lease e autorizações são preservadas.
67
+
68
+ Se qualquer validação ou persistência falhar depois do switch, o rollback restaura a branch ou
69
+ detached HEAD anterior; uma branch criada pela tentativa também é removida.
70
+
71
+ ## Erros comuns e diagnóstico
72
+
73
+ - `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
74
+ - `WENDKEEP_CONTEXT_SCOPE_MISMATCH` ou `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: volte ao checkout
75
+ reservado ou diagnostique a sessão; o comando não adota uma divergência posterior.
76
+ - `WENDKEEP_CONTEXT_CONFLICT`: outro contexto ativo ocupa o destino; use outra branch/worktree ou
77
+ encerre corretamente o contexto concorrente.
78
+ - `WENDKEEP_CONTEXT_GIT`: corrija a branch, dirty state conflitante ou erro do Git e repita.
79
+ - `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git e registry e faça diagnóstico manual antes de
80
+ qualquer nova mutação.
81
+ - `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: troque o comando Git cru por `wendkeep context switch`.
82
+
83
+ ## Próximos passos
84
+
85
+ Veja [worktrees gerenciadas](worktrees.md) para criar checkouts isolados e
86
+ [changes e verificação](changes-and-verification.md) para continuar o lifecycle na nova branch.
@@ -115,11 +115,15 @@ function invocationOf(segment) {
115
115
  if (executable === 'git' || executable === 'git.exe' || executable === 'git.cmd') {
116
116
  return { kind: 'git', args: segment.slice(index + 1).map(unquote) };
117
117
  }
118
+ const args = segment.slice(index + 1).map(unquote);
119
+ if (isWendKeepContextSwitch(executable, args)) {
120
+ return { kind: 'context-switch', args };
121
+ }
118
122
  if (['rm', 'rm.exe', 'del', 'erase', 'remove-item', 'move-item', 'set-content', 'out-file', 'copy-item', 'new-item'].includes(executable)) {
119
- return { kind: 'filesystem', args: segment.slice(index + 1).map(unquote) };
123
+ return { kind: 'filesystem', args };
120
124
  }
121
- if (isPublicationInvocation(executable, segment.slice(index + 1).map(unquote))) {
122
- return { kind: 'publication', args: segment.slice(index + 1).map(unquote) };
125
+ if (isPublicationInvocation(executable, args)) {
126
+ return { kind: 'publication', args };
123
127
  }
124
128
  return null;
125
129
  }
@@ -133,6 +137,24 @@ function isWendKeepPublication(args) {
133
137
  return command === 'publish' || command === 'release';
134
138
  }
135
139
 
140
+ function isWendKeepContextSwitch(executable, args) {
141
+ let commandArgs = args;
142
+ if (['node', 'node.exe'].includes(executable)) {
143
+ const entrypoint = args.findIndex((arg) => executableName(arg) === 'wendkeep.mjs');
144
+ if (entrypoint < 0) return false;
145
+ commandArgs = args.slice(entrypoint + 1);
146
+ } else if (['npx', 'npx.cmd'].includes(executable)) {
147
+ const packageIndex = args.findIndex((arg) => ['wendkeep', 'wk'].includes(executableName(arg)));
148
+ if (packageIndex < 0) return false;
149
+ commandArgs = args.slice(packageIndex + 1);
150
+ } else if (!['wendkeep', 'wendkeep.cmd', 'wk', 'wk.cmd'].includes(executable)) {
151
+ return false;
152
+ }
153
+ const command = firstNonOption(commandArgs);
154
+ const commandIndex = commandArgs.findIndex((arg) => String(arg).toLowerCase() === command);
155
+ return command === 'context' && String(commandArgs[commandIndex + 1] || '').toLowerCase() === 'switch';
156
+ }
157
+
136
158
  function isPublicationInvocation(executable, args) {
137
159
  if (['npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'yarn', 'yarn.cmd', 'bun', 'bun.exe'].includes(executable)) {
138
160
  const command = firstNonOption(args);
@@ -214,12 +236,27 @@ export function scopeActionsForCommand(command) {
214
236
  const actions = [];
215
237
  for (const invocation of commandInvocations(command)) {
216
238
  if (invocation.kind === 'git') actions.push(gitAction(invocation.args));
239
+ if (invocation.kind === 'context-switch') actions.push('git:destructive');
217
240
  if (invocation.kind === 'filesystem') actions.push('filesystem:mutation');
218
241
  if (invocation.kind === 'publication') actions.push('publish');
219
242
  }
220
243
  return [...new Set(actions.filter(Boolean))];
221
244
  }
222
245
 
246
+ export function commandChangesGitBranch(command) {
247
+ return commandInvocations(command).some((invocation) => {
248
+ if (invocation.kind !== 'git') return false;
249
+ const subcommand = firstGitSubcommand(invocation.args);
250
+ if (subcommand === 'switch') return true;
251
+ if (subcommand !== 'checkout') return false;
252
+ const subcommandIndex = invocation.args.findIndex((arg) => String(arg).toLowerCase() === 'checkout');
253
+ const checkoutArgs = invocation.args.slice(subcommandIndex + 1);
254
+ if (!checkoutArgs.length || checkoutArgs.includes('--')) return false;
255
+ if (checkoutArgs.includes('-p') || checkoutArgs.includes('--patch') || checkoutArgs.includes('--help')) return false;
256
+ return true;
257
+ });
258
+ }
259
+
223
260
  export function commandChangesDirectory(command) {
224
261
  return shellSegments(command).some((segment) => {
225
262
  const executable = executableName(segment[0]);
@@ -333,7 +370,14 @@ export function concurrentScopeConflicts(expectedScope, activeSessions = [], cur
333
370
  if (!entry || String(sessionId) === String(current) || String(entry.sessionId || '') === String(current)) continue;
334
371
  if (entry.status && entry.status !== 'active') continue;
335
372
  const other = entry.project_scope || entry.projectScope || (entry.complete !== undefined ? entry : null);
336
- if (!other || other.complete !== true) {
373
+ if (!other) {
374
+ conflicts.push({ sessionId: String(sessionId), reason: 'scope-unavailable' });
375
+ continue;
376
+ }
377
+ const leftWorktree = comparableScopeValue(expectedScope, 'worktree');
378
+ const rightWorktree = comparableScopeValue(other, 'worktree');
379
+ if (leftWorktree && rightWorktree && leftWorktree !== rightWorktree) continue;
380
+ if (other.complete !== true) {
337
381
  conflicts.push({ sessionId: String(sessionId), reason: 'scope-unavailable' });
338
382
  continue;
339
383
  }
@@ -343,8 +387,6 @@ export function concurrentScopeConflicts(expectedScope, activeSessions = [], cur
343
387
  return Boolean(left && right && left === right);
344
388
  });
345
389
  if (!sameRepositoryBranch) continue;
346
- const leftWorktree = comparableScopeValue(expectedScope, 'worktree');
347
- const rightWorktree = comparableScopeValue(other, 'worktree');
348
390
  if (!leftWorktree || !rightWorktree || leftWorktree === rightWorktree) {
349
391
  conflicts.push({ sessionId: String(sessionId), reason: 'same-repository-branch' });
350
392
  }
@@ -388,6 +430,9 @@ export function scopeDecision({
388
430
  if (concurrent.length) {
389
431
  return decision(host, `WENDKEEP_SCOPE_CONFLICT: há ${concurrent.length} sessão(ões) ativa(s) com a mesma raiz Git/branch ou escopo não comprovado; use um worktree distinto ou selecione explicitamente o projeto para criar uma nova lease.`);
390
432
  }
433
+ if (commandChangesGitBranch(command)) {
434
+ return decision(host, 'WENDKEEP_CONTEXT_SWITCH_REQUIRED: troca de branch Git crua deixaria a sessão fora da scope reservada; use `wendkeep context switch <branch> [--create] [--session <id>]`.');
435
+ }
391
436
  const authorized = Array.isArray(expectedScope.authorizedActions)
392
437
  ? expectedScope.authorizedActions
393
438
  : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.76.0",
3
+ "version": "0.76.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": [
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "precheck": "node --check src/worktree.mjs && node --check packages/vault/src/worktree-metadata.mjs",
44
+ "precheck": "node --check src/worktree.mjs && node --check src/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/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",
46
46
  "test": "node --test --test-concurrency=2",
47
47
  "test:core": "node scripts/run-scope.mjs core",
@@ -57,6 +57,8 @@ Usage:
57
57
  wendkeep worktree status [slug] [--json]
58
58
  wendkeep worktree open <slug> [--editor vscode] [--json]
59
59
  Managed linked worktrees under .worktrees (branch default wk/<slug>).
60
+ wendkeep context switch <branch> [--create] [--session <id>] [--json]
61
+ Switch Git branch and the causal session scope in the same worktree.
60
62
  wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
61
63
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
62
64
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
@@ -200,6 +202,9 @@ async function main(argv) {
200
202
  } else if (cmd === 'profile') {
201
203
  const { PROFILE_HELP } = await import('../../../src/profile.mjs');
202
204
  process.stdout.write(PROFILE_HELP);
205
+ } else if (cmd === 'context') {
206
+ const { CONTEXT_HELP } = await import('../../../src/context.mjs');
207
+ process.stdout.write(CONTEXT_HELP);
203
208
  } else {
204
209
  process.stdout.write(HELP);
205
210
  }
@@ -241,6 +246,11 @@ async function main(argv) {
241
246
  process.exit(runWorktree(rest));
242
247
  break;
243
248
  }
249
+ case 'context': {
250
+ const { runContext } = await import('../../../src/context.mjs');
251
+ process.exit(runContext(rest));
252
+ break;
253
+ }
244
254
  case 'sync': {
245
255
  const { runSync } = await import('../../../src/sync.mjs');
246
256
  process.exit(await runSync(rest));
@@ -0,0 +1,297 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import {
4
+ mutateSessionRegistry,
5
+ readSessionRegistry,
6
+ } from '../hooks/obsidian-common.mjs';
7
+ import {
8
+ captureProjectScope,
9
+ compareProjectScopes,
10
+ concurrentScopeConflicts,
11
+ scopeForRegistry,
12
+ } from '../hooks/project-scope.mjs';
13
+
14
+ export const CONTEXT_HELP = `wendkeep context <subcommand>
15
+
16
+ switch <branch> [--create] [--session <id>] [--project <path>] [--vault <path>] [--json]
17
+
18
+ Switches Git branch and the causal session scope together inside the same worktree.
19
+ Without --session, exactly one active session must match the current scope.
20
+ `;
21
+
22
+ const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
23
+ const FLAG_OPTIONS = new Set(['--create', '--json']);
24
+
25
+ function contextError(code, message) {
26
+ const error = new Error(message);
27
+ error.code = code;
28
+ return error;
29
+ }
30
+
31
+ function optionValue(argv, name) {
32
+ const index = argv.indexOf(name);
33
+ if (index >= 0) return argv[index + 1] || '';
34
+ return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
35
+ }
36
+
37
+ function positionals(argv) {
38
+ const result = [];
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const value = argv[index];
41
+ if (VALUE_OPTIONS.has(value)) { index += 1; continue; }
42
+ if ([...VALUE_OPTIONS].some((name) => value.startsWith(`${name}=`))) continue;
43
+ if (FLAG_OPTIONS.has(value)) continue;
44
+ if (value.startsWith('--')) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção desconhecida: ${value}`);
45
+ result.push(value);
46
+ }
47
+ return result;
48
+ }
49
+
50
+ function validateArgv(argv) {
51
+ const seen = new Set();
52
+ for (let index = 0; index < argv.length; index += 1) {
53
+ const value = argv[index];
54
+ if (FLAG_OPTIONS.has(value)) {
55
+ if (seen.has(value)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${value}`);
56
+ seen.add(value);
57
+ continue;
58
+ }
59
+ if (VALUE_OPTIONS.has(value)) {
60
+ if (seen.has(value)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${value}`);
61
+ seen.add(value);
62
+ const next = argv[index + 1];
63
+ if (!next || next.startsWith('--')) throw contextError('WENDKEEP_CONTEXT_ARGS', `${value} requer um valor`);
64
+ index += 1;
65
+ continue;
66
+ }
67
+ if (value.startsWith('--')) {
68
+ const name = value.split('=', 1)[0];
69
+ if (!VALUE_OPTIONS.has(name)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção desconhecida: ${name}`);
70
+ if (seen.has(name)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${name}`);
71
+ seen.add(name);
72
+ if (!value.slice(name.length + 1)) throw contextError('WENDKEEP_CONTEXT_ARGS', `${name} requer um valor`);
73
+ }
74
+ }
75
+ }
76
+
77
+ function vaultOf(argv) {
78
+ const raw = optionValue(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
79
+ if (!raw) throw contextError('WENDKEEP_CONTEXT_VAULT', 'binding de Vault ausente; use --vault <path>');
80
+ return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
81
+ }
82
+
83
+ function projectOf(argv) {
84
+ const raw = optionValue(argv, '--project') || process.cwd();
85
+ return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
86
+ }
87
+
88
+ function git(projectRoot, args, spawn = spawnSync) {
89
+ const result = spawn('git', args, { cwd: projectRoot, encoding: 'utf8', windowsHide: true });
90
+ if (result.error || result.status !== 0) {
91
+ const detail = String(result.stderr || result.error?.message || 'falhou').trim();
92
+ throw contextError('WENDKEEP_CONTEXT_GIT', `git ${args.join(' ')}: ${detail}`);
93
+ }
94
+ return String(result.stdout || '').trim();
95
+ }
96
+
97
+ function actualScope(projectRoot, expected, sessionId, spawn) {
98
+ return captureProjectScope({
99
+ input: { cwd: projectRoot },
100
+ projectRoot: expected?.projectRoot || projectRoot,
101
+ projectId: expected?.projectId || '',
102
+ provider: expected?.provider || '',
103
+ sessionId,
104
+ targetCwd: projectRoot,
105
+ spawn,
106
+ });
107
+ }
108
+
109
+ function matchingSessionIds(registry, projectRoot, spawn) {
110
+ const matches = [];
111
+ for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
112
+ if (entry?.status !== 'active' || entry.project_scope_conflict === true || !entry?.project_scope) continue;
113
+ const actual = actualScope(projectRoot, entry.project_scope, sessionId, spawn);
114
+ if (compareProjectScopes(entry.project_scope, actual).ok) matches.push(sessionId);
115
+ }
116
+ return matches;
117
+ }
118
+
119
+ function contextRevision(entry) {
120
+ return Number.isSafeInteger(entry?.context_revision) && entry.context_revision >= 0
121
+ ? entry.context_revision : 0;
122
+ }
123
+
124
+ function resolveSessionId(vaultBase, projectRoot, requested, spawn) {
125
+ const registry = readSessionRegistry(vaultBase);
126
+ if (requested) {
127
+ const entry = registry.sessions?.[requested];
128
+ if (!entry || entry.status !== 'active') {
129
+ throw contextError('WENDKEEP_CONTEXT_SESSION', `sessão ativa não encontrada: ${requested}`);
130
+ }
131
+ return { sessionId: requested, revision: contextRevision(entry) };
132
+ }
133
+ const matches = matchingSessionIds(registry, projectRoot, spawn);
134
+ if (matches.length !== 1) {
135
+ throw contextError(
136
+ 'WENDKEEP_CONTEXT_AMBIGUOUS',
137
+ `${matches.length} sessões ativas correspondem à scope atual; informe --session <id>.`,
138
+ );
139
+ }
140
+ return {
141
+ sessionId: matches[0],
142
+ revision: contextRevision(registry.sessions[matches[0]]),
143
+ };
144
+ }
145
+
146
+ function rollbackGit(projectRoot, previous, createdBranch, spawn) {
147
+ const args = previous.branch.startsWith('detached:')
148
+ ? ['switch', '--detach', previous.head]
149
+ : ['switch', previous.branch];
150
+ git(projectRoot, args, spawn);
151
+ if (createdBranch) git(projectRoot, ['branch', '-D', createdBranch], spawn);
152
+ }
153
+
154
+ export function switchSessionContext({
155
+ vaultBase,
156
+ projectRoot = process.cwd(),
157
+ branch,
158
+ create = false,
159
+ sessionId = '',
160
+ spawn = spawnSync,
161
+ mutateRegistry = mutateSessionRegistry,
162
+ now = () => new Date(),
163
+ } = {}) {
164
+ const target = String(branch || '').trim();
165
+ if (!target) throw contextError('WENDKEEP_CONTEXT_ARGS', 'switch requer <branch>');
166
+ git(projectRoot, ['check-ref-format', '--branch', target], spawn);
167
+ const selected = resolveSessionId(vaultBase, projectRoot, sessionId, spawn);
168
+ const selectedSessionId = selected.sessionId;
169
+ let switched = false;
170
+ let previous = null;
171
+
172
+ try {
173
+ return mutateRegistry(vaultBase, (registry) => {
174
+ const entry = registry.sessions?.[selectedSessionId];
175
+ if (!entry || entry.status !== 'active') {
176
+ throw contextError('WENDKEEP_CONTEXT_SESSION', `sessão ativa não encontrada: ${selectedSessionId}`);
177
+ }
178
+ if (entry.project_scope_conflict === true || !entry.project_scope) {
179
+ throw contextError('WENDKEEP_CONTEXT_SCOPE_CONFLICT', 'a sessão possui scope ausente ou conflitante');
180
+ }
181
+ const currentRevision = contextRevision(entry);
182
+ if (currentRevision !== selected.revision) {
183
+ throw contextError(
184
+ 'WENDKEEP_CONTEXT_CAS_MISMATCH',
185
+ `context_revision mudou de ${selected.revision} para ${currentRevision}; repita com estado fresco`,
186
+ );
187
+ }
188
+ const expected = entry.project_scope;
189
+ const actual = actualScope(projectRoot, expected, selectedSessionId, spawn);
190
+ const comparison = compareProjectScopes(expected, actual);
191
+ if (!comparison.ok) {
192
+ throw contextError(
193
+ 'WENDKEEP_CONTEXT_SCOPE_MISMATCH',
194
+ `scope atual diverge da reserva (${comparison.mismatches.join(', ')})`,
195
+ );
196
+ }
197
+ if (actual.branch === target && !create) {
198
+ return {
199
+ status: 'unchanged', session_id: selectedSessionId, branch: target,
200
+ head: actual.head, revision: currentRevision,
201
+ };
202
+ }
203
+
204
+ previous = { branch: actual.branch, head: actual.head };
205
+ git(projectRoot, create ? ['switch', '-c', target] : ['switch', target], spawn);
206
+ switched = true;
207
+ const next = actualScope(projectRoot, expected, selectedSessionId, spawn);
208
+ const identity = compareProjectScopes({ ...expected, branch: next.branch }, next);
209
+ if (!identity.ok || next.branch !== target || next.worktree !== actual.worktree) {
210
+ const mismatches = [...identity.mismatches, ...(next.branch === target ? [] : ['scope.branch'])];
211
+ throw contextError(
212
+ 'WENDKEEP_CONTEXT_IDENTITY_CHANGED',
213
+ `a transição saiu da identidade reservada (${[...new Set(mismatches)].join(', ')})`,
214
+ );
215
+ }
216
+ const conflicts = concurrentScopeConflicts(
217
+ next,
218
+ Object.entries(registry.sessions || {}).filter(([, candidate]) => candidate?.status === 'active'),
219
+ selectedSessionId,
220
+ );
221
+ if (conflicts.length) {
222
+ throw contextError(
223
+ 'WENDKEEP_CONTEXT_CONFLICT',
224
+ `o destino conflita com ${conflicts.length} contexto(s) ativo(s): ${conflicts.map((item) => item.sessionId).join(', ')}`,
225
+ );
226
+ }
227
+
228
+ const revision = currentRevision + 1;
229
+ const at = now().toISOString();
230
+ const transition = {
231
+ revision,
232
+ operation: create ? 'create' : 'switch',
233
+ from: { branch: actual.branch, head: actual.head },
234
+ to: { branch: next.branch, head: next.head },
235
+ worktree: next.worktree,
236
+ at,
237
+ };
238
+ const {
239
+ project_scope_conflict: _conflict,
240
+ project_scope_conflict_fields: _conflictFields,
241
+ project_scope_observed: _observed,
242
+ ...preserved
243
+ } = entry;
244
+ registry.sessions[selectedSessionId] = {
245
+ ...preserved,
246
+ project_scope: scopeForRegistry(next, { authorizedActions: expected.authorizedActions }),
247
+ context_revision: revision,
248
+ context_transitions: [...(Array.isArray(entry.context_transitions) ? entry.context_transitions : []), transition],
249
+ last_seen: at,
250
+ updated_at: at,
251
+ };
252
+ return {
253
+ status: 'switched', session_id: selectedSessionId, branch: next.branch,
254
+ head: next.head, revision, transition,
255
+ };
256
+ });
257
+ } catch (error) {
258
+ if (switched && previous) {
259
+ try {
260
+ rollbackGit(projectRoot, previous, create ? target : '', spawn);
261
+ } catch (rollbackError) {
262
+ throw contextError(
263
+ 'WENDKEEP_CONTEXT_ROLLBACK_FAILED',
264
+ `${error.code || 'WENDKEEP_CONTEXT_FAILED'}: ${error.message}; rollback: ${rollbackError.message}`,
265
+ );
266
+ }
267
+ }
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ function output(result, json) {
273
+ if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
274
+ else process.stdout.write(`context ${result.status}: ${result.branch} (session ${result.session_id}; revision ${result.revision})\n`);
275
+ }
276
+
277
+ export function runContext(argv = []) {
278
+ try {
279
+ validateArgv(argv);
280
+ const [sub, branch, ...extra] = positionals(argv);
281
+ if (sub !== 'switch' || !branch || extra.length) {
282
+ throw contextError('WENDKEEP_CONTEXT_ARGS', 'use: wendkeep context switch <branch> [--create] [--session <id>]');
283
+ }
284
+ const result = switchSessionContext({
285
+ vaultBase: vaultOf(argv),
286
+ projectRoot: projectOf(argv),
287
+ branch,
288
+ create: argv.includes('--create'),
289
+ sessionId: optionValue(argv, '--session'),
290
+ });
291
+ output(result, argv.includes('--json'));
292
+ return 0;
293
+ } catch (error) {
294
+ process.stderr.write(`wendkeep context: ${error.code || 'WENDKEEP_CONTEXT_FAILED'}: ${error.message}\n`);
295
+ return 2;
296
+ }
297
+ }