wendkeep 0.76.2 → 0.76.4

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,30 @@ 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.4] — 2026-08-22
8
+
9
+ ### Added
10
+
11
+ - **Registry multi-contexto.** `active_contexts` mantém change, branch e HEAD por
12
+ `repository_id` + `worktree_id` + `work_session_id`, com schema, revisão, CAS e writes atômicos;
13
+ duas worktrees ou sessões deixam de sobrescrever o foco operacional umas das outras.
14
+ - **Resolução causal de change.** Change, spec e verify usam a sessão/worktree chamadora; ausência
15
+ de sessão só aceita um contexto inequívoco e qualquer ambiguidade falha fechado.
16
+ - **Compatibilidade conservadora.** `CURRENT_CHANGE.md` é somente projeção quando há um único
17
+ contexto ativo, e a migração legada não inventa worktree ou sessão quando a identidade não pode
18
+ ser provada.
19
+
20
+ ## [0.76.3] — 2026-08-21
21
+
22
+ ### Added
23
+
24
+ - **Recuperação explícita do contexto em quarentena.** `context status --session <id>` inventaria
25
+ candidatas `reserved`/`observed` sem paths locais; `context recover` exige seleção, revisão CAS,
26
+ motivo e correspondência integral com o checkout/HEAD antes de limpar o conflito.
27
+ - **Receipt pós-conflito.** A recuperação preserva change, lease e autorizações, incrementa a revisão
28
+ e registra evidência sanitizada `from/to`; revision stale, identidade divergente ou falha de
29
+ persistência mantêm registry e quarentena intactos.
30
+
7
31
  ## [0.76.2] — 2026-08-21
8
32
 
9
33
  ### Fixed
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. 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. 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. 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
  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,7 +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
+ | **Active context** | `active_contexts` by `repository_id`/`worktree_id`/`work_session_id`, causal transition, quarantine, and explicit recovery | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
243
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) |
244
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) |
245
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. 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**.
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. 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
  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,7 +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
+ | **Active context** | `active_contexts` by `repository_id`/`worktree_id`/`work_session_id`, causal transition, quarantine, and explicit recovery | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
243
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) |
244
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) |
245
245
  | **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
@@ -25,12 +25,12 @@ Initialize the project, keep the vault healthy, and provide a valid `wendkeep.se
25
25
  ## Syntax
26
26
 
27
27
  ```bash
28
- npx wendkeep change new <slug> [--simple|--guide]
29
- npx wendkeep change status [slug]
30
- npx wendkeep spec effective --change <slug>
28
+ npx wendkeep change new <slug> [--simple|--guide] [--session <id>]
29
+ npx wendkeep change status [slug] [--session <id>]
30
+ npx wendkeep spec effective [--change <slug>] [--session <id>]
31
31
  npx wendkeep sensors list
32
- npx wendkeep verify [--deep] [--change <slug>]
33
- npx wendkeep change archive <slug>
32
+ npx wendkeep verify [--deep] [--change <slug>] [--session <id>]
33
+ npx wendkeep change archive <slug> [--session <id>]
34
34
  ```
35
35
 
36
36
  ## Options and exit codes
@@ -43,6 +43,8 @@ npx wendkeep change archive <slug>
43
43
  without archiving it.
44
44
  - `change continue <archived> <new>` starts follow-up work without inheriting stale proof.
45
45
  - `change bind <slug> --session <id>` attaches an existing session.
46
+ - `--session <id>` selects the causal `active_contexts` entry for implicit commands. Without it,
47
+ only one unambiguous active context for the worktree is accepted; ambiguity returns exit `2`.
46
48
  - `change relink [--apply]` and `change backlink [--apply]` repair graph links; preview is default.
47
49
  - `change abandon <slug>` drops work without an ADR; `archive --force` needs explicit human choice.
48
50
  - `wendkeep spec list|show|effective|migrate|rebase` manages living contracts and deltas.
@@ -54,6 +56,7 @@ npx wendkeep change archive <slug>
54
56
 
55
57
  ```bash
56
58
  npx wendkeep change new tenant-login
59
+ npx wendkeep change use tenant-login --session <id>
57
60
  npx wendkeep change new internal-adjustment --guide
58
61
  npx wendkeep spec effective --change tenant-login
59
62
  npx wendkeep change done 1.1 --change tenant-login
@@ -82,6 +85,10 @@ tool runs, it compares the session, project, Git root, remote, branch, and workt
82
85
  recorded in `SESSION_REGISTRY.json`. Missing, ambiguous, concurrent, or cross-project targets are
83
86
  blocked before the tool.
84
87
 
88
+ Implicit change focus comes from `active_contexts`, not `CURRENT_CHANGE.md`. Its key combines
89
+ `repository_id`, `worktree_id`, and `work_session_id`; the Markdown pointer remains only a
90
+ compatibility projection when there is one unambiguous context.
91
+
85
92
  The [local Observer](observer.md) is a read-only observability projection: the vault and change
86
93
  remain local authorities. Observer queries do not complete, archive, repair, or promote state in a
87
94
  vault.
@@ -4,18 +4,19 @@
4
4
 
5
5
  ## Purpose
6
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.
7
+ Inspect and move the same session's causal scope with proof of the current checkout: during a
8
+ normal branch transition or when explicitly recovering a divergence already under quarantine.
9
9
 
10
10
  ## When to use
11
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.
12
+ Use `context switch` to create or select another branch in the same worktree. If the registry
13
+ already records `project_scope_conflict`, use `context status` to inventory `reserved` and
14
+ `observed` without local paths; recover only through `context recover` and an explicit human choice.
14
15
 
15
16
  ## When not to use
16
17
 
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.
18
+ Do not use it to move to another worktree, hand-edit the registry, or replace `worktree create`.
19
+ Recovery never selects a candidate automatically or accepts a scope that no longer matches HEAD.
19
20
 
20
21
  ## Prerequisites
21
22
 
@@ -27,6 +28,8 @@ or replace `worktree create`. Those cases require a separate physical context or
27
28
 
28
29
  ```bash
29
30
  npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <root>] [--vault <vault>] [--json]
31
+ npx --no-install wendkeep context status --session <id> [--project <root>] [--vault <vault>] [--json]
32
+ npx --no-install wendkeep context recover --session <id> --select <reserved|observed> --revision <n> --reason <text> [--project <root>] [--vault <vault>] [--json]
30
33
  ```
31
34
 
32
35
  Without `--session`, exactly one active session must fully match the current scope. `--create`
@@ -36,6 +39,9 @@ uses `git switch -c`; without it, the command follows `git switch` semantics.
36
39
 
37
40
  - `--create`: create the branch from the current HEAD.
38
41
  - `--session <id>`: select the causal session explicitly; recommended whenever selection is unclear.
42
+ - `--select <reserved|observed>`: select exactly one quarantined candidate; required for recovery.
43
+ - `--revision <n>`: CAS against the revision returned by `context status`; required for recovery.
44
+ - `--reason <text>`: auditable reason, sanitized and limited to 240 characters.
39
45
  - `--project <root>` and `--vault <vault>`: select the binding and paths for manual use.
40
46
  - `--json`: emit status, session id, branch, HEAD, revision, and event without exposing the Vault.
41
47
 
@@ -49,6 +55,8 @@ ambiguity, scope mismatch, conflict, Git failure, or rollback returns `2` with a
49
55
  npx --no-install wendkeep context switch wk/auth --create
50
56
  npx --no-install wendkeep context switch main --session 019abc-session-id
51
57
  npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
58
+ npx --no-install wendkeep context status --session 019abc-session-id --json
59
+ npx --no-install wendkeep context recover --session 019abc-session-id --select observed --revision 7 --reason "checkout confirmed"
52
60
  ```
53
61
 
54
62
  Do not replace it with the raw command below while the harness is active:
@@ -70,11 +78,38 @@ authorizations are preserved.
70
78
  If validation or persistence fails after the switch, rollback restores the previous branch or
71
79
  detached HEAD; a branch created by the failed attempt is removed as well.
72
80
 
81
+ During recovery, both candidates must be complete and retain the same causal identity. The selected
82
+ candidate must fully match the current project, repository, remote, worktree, branch, and HEAD.
83
+ Under the registry lock, the command revalidates the revision, increments `context_revision`,
84
+ preserves the change/lease/authorizations, clears only the quarantine, and appends a sanitized
85
+ receipt to `context_recoveries`. Any failure leaves the registry and quarantine byte-identical.
86
+ This is fail-closed: no candidate, receipt, or partial scope is published after a failed check.
87
+
88
+ ### Multi-context change registry
89
+
90
+ `SESSION_REGISTRY.json` keeps `active_contexts` with its own schema and revision. Each entry is
91
+ identified by `repository_id` + `worktree_id` + `work_session_id`; branch, HEAD, and `change_slug`
92
+ belong to that entry. Two worktrees can therefore select different changes without overwriting
93
+ each other's operational focus.
94
+
95
+ With an explicit causal session, change, spec, and verify resolve only the matching entry. Without
96
+ a session, only one active entry for the worktree is accepted; two sessions produce ambiguity and
97
+ the operation must fail closed without silently selecting a change.
98
+
99
+ `CURRENT_CHANGE.md` is only a derived projection: it contains a change when there is one single,
100
+ unambiguous active context. With zero or multiple contexts it stays empty. Migration is conservative
101
+ and never invents a worktree or session identity. The legacy pointer becomes a context only
102
+ when one active session, a complete scope, and worktree metadata prove one identity.
103
+
73
104
  ## Common errors and diagnosis
74
105
 
75
106
  - `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.
107
+ - `WENDKEEP_CONTEXT_SCOPE_MISMATCH`: the selected candidate does not prove the current checkout/HEAD;
108
+ run `context status` again and select only a candidate with `matches_actual: true`.
109
+ - `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: the session is not quarantined or a candidate is missing.
110
+ - `WENDKEEP_CONTEXT_CAS_MISMATCH`: the revision changed; discard the stale decision and rerun status.
111
+ - `WENDKEEP_CONTEXT_IDENTITY_CHANGED`: candidates belong to different causal identities; preserve
112
+ quarantine and diagnose the registry.
78
113
  - `WENDKEEP_CONTEXT_CONFLICT`: another active context occupies the target; use another
79
114
  branch/worktree or close the competing context correctly.
80
115
  - `WENDKEEP_CONTEXT_GIT`: fix the branch, conflicting dirty state, or Git error and retry.
@@ -25,12 +25,12 @@ Tenha o projeto inicializado, um vault saudável e `wendkeep.sensors.json` váli
25
25
  ## Sintaxe
26
26
 
27
27
  ```bash
28
- npx wendkeep change new <slug> [--simple|--guide]
29
- npx wendkeep change status [slug]
30
- npx wendkeep spec effective --change <slug>
28
+ npx wendkeep change new <slug> [--simple|--guide] [--session <id>]
29
+ npx wendkeep change status [slug] [--session <id>]
30
+ npx wendkeep spec effective [--change <slug>] [--session <id>]
31
31
  npx wendkeep sensors list
32
- npx wendkeep verify [--deep] [--change <slug>]
33
- npx wendkeep change archive <slug>
32
+ npx wendkeep verify [--deep] [--change <slug>] [--session <id>]
33
+ npx wendkeep change archive <slug> [--session <id>]
34
34
  ```
35
35
 
36
36
  ## Opções e códigos de saída
@@ -43,6 +43,8 @@ npx wendkeep change archive <slug>
43
43
  trabalho sem arquivar.
44
44
  - `change continue <arquivada> <nova>` abre continuação sem herdar evidência antiga.
45
45
  - `change bind <slug> --session <id>` liga uma sessão existente.
46
+ - `--session <id>` seleciona o `active_contexts` causal nos comandos implícitos. Sem a opção,
47
+ somente um contexto ativo inequívoco da worktree é aceito; ambiguidade retorna exit `2`.
46
48
  - `change relink [--apply]` e `change backlink [--apply]` reparam o grafo; dry-run é o padrão.
47
49
  - `change abandon <slug>` descarta sem ADR; `archive --force` exige decisão humana explícita.
48
50
  - `wendkeep spec list|show|effective|migrate|rebase` administra contratos vivos e deltas.
@@ -54,6 +56,7 @@ npx wendkeep change archive <slug>
54
56
 
55
57
  ```bash
56
58
  npx wendkeep change new login-tenant
59
+ npx wendkeep change use login-tenant --session <id>
57
60
  npx wendkeep change new ajuste-interno --guide
58
61
  npx wendkeep spec effective --change login-tenant
59
62
  npx wendkeep change done 1.1 --change login-tenant
@@ -82,6 +85,10 @@ uma ferramenta de escrita suportada, ele compara sessão, projeto, raiz Git, rem
82
85
  worktree com a lease registrada no `SESSION_REGISTRY.json`. Um alvo ausente, ambíguo, concorrente ou
83
86
  fora do projeto é bloqueado antes da ferramenta.
84
87
 
88
+ O foco implícito de change vem de `active_contexts`, não de `CURRENT_CHANGE.md`. A chave combina
89
+ `repository_id`, `worktree_id` e `work_session_id`; o ponteiro Markdown permanece apenas como
90
+ projeção compatível quando existe um único contexto inequívoco.
91
+
85
92
  O [Observer local](observer.md) é uma projeção read-only da observabilidade: o vault e a change
86
93
  continuam autoridades locais. Consultas do Observer não concluem, arquivam, reparam ou promovem
87
94
  estado no vault.
@@ -4,18 +4,19 @@
4
4
 
5
5
  ## Objetivo
6
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.
7
+ Inspecionar e mover a scope causal da mesma sessão com prova do checkout atual: durante uma troca
8
+ de branch normal ou ao recuperar explicitamente uma divergência já colocada em quarentena.
9
9
 
10
10
  ## Quando usar
11
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.
12
+ Use `context switch` para criar ou selecionar outra branch na mesma worktree. Se o registry já
13
+ registrou `project_scope_conflict`, use `context status` para inventariar `reserved` e `observed`
14
+ sem paths locais; recupere somente com `context recover` e uma seleção humana explícita.
14
15
 
15
16
  ## Quando não usar
16
17
 
17
- Não use para mudar de worktree, adotar uma scope divergente, reparar o registry ou substituir
18
- `worktree create`. Esses casos exigem outro contexto físico ou diagnóstico explícito.
18
+ Não use para mudar de worktree, editar o registry à mão ou substituir `worktree create`. Recovery
19
+ não escolhe a candidata automaticamente e não aceita uma scope que deixou de corresponder ao HEAD.
19
20
 
20
21
  ## Pré-requisitos
21
22
 
@@ -27,6 +28,8 @@ Não use para mudar de worktree, adotar uma scope já divergente, reparar o regi
27
28
 
28
29
  ```bash
29
30
  npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <raiz>] [--vault <cofre>] [--json]
31
+ npx --no-install wendkeep context status --session <id> [--project <raiz>] [--vault <cofre>] [--json]
32
+ npx --no-install wendkeep context recover --session <id> --select <reserved|observed> --revision <n> --reason <texto> [--project <raiz>] [--vault <cofre>] [--json]
30
33
  ```
31
34
 
32
35
  Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à scope atual. Use
@@ -36,6 +39,9 @@ Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à
36
39
 
37
40
  - `--create`: cria a branch a partir do HEAD atual.
38
41
  - `--session <id>`: seleciona explicitamente a sessão causal; recomendado quando houver dúvida.
42
+ - `--select <reserved|observed>`: escolhe exatamente uma candidata da quarentena; obrigatório no recovery.
43
+ - `--revision <n>`: CAS contra a revisão exibida por `context status`; obrigatório no recovery.
44
+ - `--reason <texto>`: justificativa auditável, sanitizada e limitada a 240 caracteres.
39
45
  - `--project <raiz>` e `--vault <cofre>`: selecionam binding e paths para uso manual.
40
46
  - `--json`: emite status, session id, branch, HEAD, revisão e evento sem expor o Vault.
41
47
 
@@ -48,6 +54,8 @@ scope divergente, conflito, falha Git ou rollback retorna `2` com um código `WE
48
54
  npx --no-install wendkeep context switch wk/auth --create
49
55
  npx --no-install wendkeep context switch main --session 019abc-session-id
50
56
  npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
57
+ npx --no-install wendkeep context status --session 019abc-session-id --json
58
+ npx --no-install wendkeep context recover --session 019abc-session-id --select observed --revision 7 --reason "checkout confirmado"
51
59
  ```
52
60
 
53
61
  Não substitua pelo comando cru abaixo quando o harness estiver ativo:
@@ -68,11 +76,37 @@ worktree, provider e session id não mudaram, incrementa `context_revision` e an
68
76
  Se qualquer validação ou persistência falhar depois do switch, o rollback restaura a branch ou
69
77
  detached HEAD anterior; uma branch criada pela tentativa também é removida.
70
78
 
79
+ No recovery, ambas as candidatas precisam ser completas e manter a mesma identidade causal. A
80
+ selecionada deve corresponder integralmente a projeto, repositório, remoto, worktree, branch e HEAD
81
+ atuais. Sob o lock do registry, o comando revalida a revisão, incrementa `context_revision`, preserva
82
+ change/lease/autorizações, limpa somente a quarentena e anexa um receipt sanitizado em
83
+ `context_recoveries`. Qualquer falha deixa registry e quarentena byte a byte intactos.
84
+
85
+ ### Registry multi-contexto de changes
86
+
87
+ O `SESSION_REGISTRY.json` mantém `active_contexts` com schema e revisão próprios. Cada entrada é
88
+ identificada por `repository_id` + `worktree_id` + `work_session_id`; branch, HEAD e `change_slug`
89
+ pertencem a essa entrada. Assim, duas worktrees podem selecionar changes diferentes sem
90
+ sobrescrever o foco uma da outra.
91
+
92
+ Com sessão causal explícita, change, spec e verify resolvem somente a entrada correspondente. Sem
93
+ sessão, uma única entrada ativa da worktree pode ser usada; duas sessões compatíveis causam
94
+ ambiguidade e a operação falha fechada, sem escolher uma change em silêncio.
95
+
96
+ `CURRENT_CHANGE.md` é apenas uma projeção derivada: contém a change somente quando existe um único
97
+ contexto ativo inequívoco. Com zero ou múltiplos contextos, fica vazio. A migração é conservadora:
98
+ não inventa uma identidade de worktree ou sessão. O ponteiro legado só vira contexto quando uma sessão
99
+ ativa, scope completa e metadados da worktree provam uma única identidade.
100
+
71
101
  ## Erros comuns e diagnóstico
72
102
 
73
103
  - `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.
104
+ - `WENDKEEP_CONTEXT_SCOPE_MISMATCH`: a candidata escolhida não prova o checkout/HEAD atual; rode
105
+ `context status` novamente e selecione apenas uma candidata com `matches_actual: true`.
106
+ - `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: a sessão não está em quarentena ou uma candidata está ausente.
107
+ - `WENDKEEP_CONTEXT_CAS_MISMATCH`: a revisão mudou; descarte a decisão antiga e repita o status.
108
+ - `WENDKEEP_CONTEXT_IDENTITY_CHANGED`: as candidatas pertencem a identidades causais diferentes;
109
+ preserve a quarentena e diagnostique o registry.
76
110
  - `WENDKEEP_CONTEXT_CONFLICT`: outro contexto ativo ocupa o destino; use outra branch/worktree ou
77
111
  encerre corretamente o contexto concorrente.
78
112
  - `WENDKEEP_CONTEXT_GIT`: corrija a branch, dirty state conflitante ou erro do Git e repita.
@@ -0,0 +1,232 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import { mutateSessionRegistry, readSessionRegistry } from './obsidian-common.mjs';
5
+ import { mkdirVaultPath, writeVaultFileSync } from './vault-path-safety.mjs';
6
+
7
+ export const ACTIVE_CONTEXTS_SCHEMA_VERSION = 1;
8
+ const POINTER = '.brain/CURRENT_CHANGE.md';
9
+ const ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
10
+ const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
11
+
12
+ function contextError(code, message) {
13
+ const error = new Error(message);
14
+ error.code = code;
15
+ return error;
16
+ }
17
+
18
+ function requiredId(value, label) {
19
+ const normalized = String(value || '').trim();
20
+ if (!ID_PATTERN.test(normalized)) {
21
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_INVALID', `${label} inválido ou ausente`);
22
+ }
23
+ return normalized;
24
+ }
25
+
26
+ function optionalText(value, maxLength = 240) {
27
+ const normalized = String(value || '').trim();
28
+ return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength);
29
+ }
30
+
31
+ function normalizeIdentity(identity = {}, { requireWorkSession = true } = {}) {
32
+ const workSessionId = requireWorkSession
33
+ ? requiredId(identity.workSessionId ?? identity.work_session_id, 'work_session_id')
34
+ : optionalText(identity.workSessionId ?? identity.work_session_id, 160);
35
+ return {
36
+ projectId: requiredId(identity.projectId ?? identity.project_id, 'project_id'),
37
+ repositoryId: requiredId(identity.repositoryId ?? identity.repository_id, 'repository_id'),
38
+ worktreeId: requiredId(identity.worktreeId ?? identity.worktree_id, 'worktree_id'),
39
+ workSessionId,
40
+ branch: optionalText(identity.branch, 240),
41
+ headSha: optionalText(identity.headSha ?? identity.head_sha, 80),
42
+ };
43
+ }
44
+
45
+ export function activeContextKey(identity) {
46
+ const normalized = normalizeIdentity(identity);
47
+ return `${normalized.repositoryId}:${normalized.worktreeId}:${normalized.workSessionId}`;
48
+ }
49
+
50
+ function contextsOf(registry) {
51
+ return registry?.active_contexts && typeof registry.active_contexts === 'object'
52
+ && !Array.isArray(registry.active_contexts)
53
+ ? registry.active_contexts : {};
54
+ }
55
+
56
+ function currentGlobalRevision(registry) {
57
+ const value = Number(registry?.active_contexts_revision);
58
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
59
+ }
60
+
61
+ function currentContextRevision(context) {
62
+ const value = Number(context?.revision);
63
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
64
+ }
65
+
66
+ function activeContexts(registry) {
67
+ return Object.entries(contextsOf(registry)).filter(([, context]) => context?.state === 'active');
68
+ }
69
+
70
+ function legacyProjection(registry) {
71
+ const active = activeContexts(registry);
72
+ if (active.length !== 1) return '';
73
+ return String(active[0][1]?.change_slug || '').trim();
74
+ }
75
+
76
+ export function projectLegacyActiveChange(vaultBase, registry = readSessionRegistry(vaultBase)) {
77
+ const slug = legacyProjection(registry);
78
+ mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz de controle da change' });
79
+ writeVaultFileSync(
80
+ vaultBase,
81
+ join(vaultBase, POINTER),
82
+ slug ? `change: ${slug}\n` : 'change:\n',
83
+ 'utf8',
84
+ { label: 'projeção legada CURRENT_CHANGE.md' },
85
+ );
86
+ return slug;
87
+ }
88
+
89
+ export function resolveActiveContext(vaultBase, query = {}) {
90
+ const normalized = normalizeIdentity(query, { requireWorkSession: false });
91
+ const registry = readSessionRegistry(vaultBase);
92
+ let workSessionId = normalized.workSessionId;
93
+ const sessionId = optionalText(query.sessionId ?? query.session_id, 160);
94
+ if (!workSessionId && sessionId) {
95
+ const entry = registry.sessions?.[sessionId];
96
+ if (entry?.status !== 'active' || !entry.work_session_id) {
97
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'sessão causal ativa não encontrada');
98
+ }
99
+ workSessionId = String(entry.work_session_id);
100
+ }
101
+
102
+ const candidates = activeContexts(registry).filter(([, context]) => (
103
+ context.project_id === normalized.projectId
104
+ && context.repository_id === normalized.repositoryId
105
+ && context.worktree_id === normalized.worktreeId
106
+ && (!workSessionId || context.work_session_id === workSessionId)
107
+ ));
108
+ if (!candidates.length) {
109
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'active context não encontrado para a identidade informada');
110
+ }
111
+ if (candidates.length > 1) {
112
+ throw contextError(
113
+ 'WENDKEEP_ACTIVE_CONTEXT_AMBIGUOUS',
114
+ 'mais de um active context corresponde à worktree; informe a sessão causal',
115
+ );
116
+ }
117
+ return structuredClone(candidates[0][1]);
118
+ }
119
+
120
+ export function mutateActiveContext(vaultBase, identity, updater, {
121
+ expectedRevision,
122
+ now = new Date().toISOString(),
123
+ mutateRegistry = mutateSessionRegistry,
124
+ } = {}) {
125
+ const normalized = normalizeIdentity(identity);
126
+ const key = activeContextKey(normalized);
127
+ const result = mutateRegistry(vaultBase, (registry) => {
128
+ const contexts = contextsOf(registry);
129
+ const existing = contexts[key] || null;
130
+ const revision = currentContextRevision(existing);
131
+ if (expectedRevision !== undefined && Number(expectedRevision) !== revision) {
132
+ throw contextError(
133
+ 'WENDKEEP_ACTIVE_CONTEXT_STALE',
134
+ `active context revision mudou de ${expectedRevision} para ${revision}`,
135
+ );
136
+ }
137
+ const base = existing || {
138
+ project_id: normalized.projectId,
139
+ repository_id: normalized.repositoryId,
140
+ worktree_id: normalized.worktreeId,
141
+ work_session_id: normalized.workSessionId,
142
+ branch: normalized.branch,
143
+ head_sha: normalized.headSha,
144
+ change_slug: '',
145
+ state: 'active',
146
+ revision: 0,
147
+ updated_at: String(now),
148
+ };
149
+ const updated = updater(structuredClone(base));
150
+ if (!updated || typeof updated !== 'object' || Array.isArray(updated)) {
151
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_INVALID', 'mutação retornou active context inválido');
152
+ }
153
+ for (const [field, expected] of [
154
+ ['project_id', normalized.projectId],
155
+ ['repository_id', normalized.repositoryId],
156
+ ['worktree_id', normalized.worktreeId],
157
+ ['work_session_id', normalized.workSessionId],
158
+ ]) {
159
+ if (updated[field] !== expected) {
160
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', `${field} não pode mudar`);
161
+ }
162
+ }
163
+ const next = {
164
+ ...updated,
165
+ branch: normalized.branch || optionalText(updated.branch, 240),
166
+ head_sha: normalized.headSha || optionalText(updated.head_sha, 80),
167
+ state: updated.state === 'closed' ? 'closed' : 'active',
168
+ revision: revision + 1,
169
+ updated_at: String(now),
170
+ };
171
+ registry.active_contexts_schema = ACTIVE_CONTEXTS_SCHEMA_VERSION;
172
+ registry.active_contexts_revision = currentGlobalRevision(registry) + 1;
173
+ registry.active_contexts = { ...contexts, [key]: next };
174
+ return { key, context: structuredClone(next), registryRevision: registry.active_contexts_revision };
175
+ });
176
+ projectLegacyActiveChange(vaultBase);
177
+ return result;
178
+ }
179
+
180
+ export function setActiveContextChange(vaultBase, identity, slug, options = {}) {
181
+ const normalizedSlug = String(slug || '').trim();
182
+ if (!SLUG_PATTERN.test(normalizedSlug)) {
183
+ throw contextError('WENDKEEP_ACTIVE_CONTEXT_CHANGE_INVALID', 'change_slug inválido ou ausente');
184
+ }
185
+ return mutateActiveContext(vaultBase, identity, (context) => ({
186
+ ...context,
187
+ change_slug: normalizedSlug,
188
+ state: 'active',
189
+ }), options);
190
+ }
191
+
192
+ export function clearActiveContextChange(vaultBase, identity, options = {}) {
193
+ return mutateActiveContext(vaultBase, identity, (context) => ({
194
+ ...context,
195
+ change_slug: '',
196
+ state: 'active',
197
+ }), options);
198
+ }
199
+
200
+ function legacyPointer(vaultBase) {
201
+ try {
202
+ return readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m)?.[1]?.trim() || '';
203
+ } catch {
204
+ return '';
205
+ }
206
+ }
207
+
208
+ export function migrateLegacyActiveContext(vaultBase, {
209
+ identityForSession,
210
+ now = new Date().toISOString(),
211
+ } = {}) {
212
+ const slug = legacyPointer(vaultBase);
213
+ if (!slug) return { migrated: false, reason: 'no-pointer' };
214
+ const registry = readSessionRegistry(vaultBase);
215
+ if (Object.keys(contextsOf(registry)).length) return { migrated: false, reason: 'already-initialized' };
216
+ if (typeof identityForSession !== 'function') return { migrated: false, reason: 'identity-unavailable' };
217
+
218
+ const candidates = [];
219
+ for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
220
+ if (entry?.status !== 'active' || entry?.project_scope?.complete !== true || !entry?.work_session_id) continue;
221
+ let candidate;
222
+ try { candidate = normalizeIdentity(identityForSession(sessionId, entry)); } catch { continue; }
223
+ if (candidate.projectId !== entry.project_scope.projectId
224
+ || candidate.workSessionId !== String(entry.work_session_id)) continue;
225
+ candidates.push(candidate);
226
+ }
227
+ if (candidates.length !== 1) {
228
+ return { migrated: false, reason: candidates.length ? 'ambiguous' : 'identity-unavailable' };
229
+ }
230
+ const result = setActiveContextChange(vaultBase, candidates[0], slug, { now });
231
+ return { migrated: true, key: result.key, context: result.context };
232
+ }