wendkeep 0.76.2 → 0.76.3
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 +11 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/en/commands/context.md +27 -8
- package/docs/pt-BR/commands/context.md +26 -8
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +3 -0
- package/src/context.mjs +276 -1
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.76.3] — 2026-08-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Recuperação explícita do contexto em quarentena.** `context status --session <id>` inventaria
|
|
12
|
+
candidatas `reserved`/`observed` sem paths locais; `context recover` exige seleção, revisão CAS,
|
|
13
|
+
motivo e correspondência integral com o checkout/HEAD antes de limpar o conflito.
|
|
14
|
+
- **Receipt pós-conflito.** A recuperação preserva change, lease e autorizações, incrementa a revisão
|
|
15
|
+
e registra evidência sanitizada `from/to`; revision stale, identidade divergente ou falha de
|
|
16
|
+
persistência mantêm registry e quarentena intactos.
|
|
17
|
+
|
|
7
18
|
## [0.76.2] — 2026-08-21
|
|
8
19
|
|
|
9
20
|
### 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 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
|
|
242
|
+
| **Active context** | `context switch/status/recover`, 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 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
|
|
242
|
+
| **Active context** | `context switch/status/recover`, 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) |
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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`
|
|
13
|
-
|
|
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,
|
|
18
|
-
|
|
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,22 @@ 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
|
+
|
|
73
88
|
## Common errors and diagnosis
|
|
74
89
|
|
|
75
90
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
|
|
76
|
-
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH
|
|
77
|
-
|
|
91
|
+
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH`: the selected candidate does not prove the current checkout/HEAD;
|
|
92
|
+
run `context status` again and select only a candidate with `matches_actual: true`.
|
|
93
|
+
- `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: the session is not quarantined or a candidate is missing.
|
|
94
|
+
- `WENDKEEP_CONTEXT_CAS_MISMATCH`: the revision changed; discard the stale decision and rerun status.
|
|
95
|
+
- `WENDKEEP_CONTEXT_IDENTITY_CHANGED`: candidates belong to different causal identities; preserve
|
|
96
|
+
quarantine and diagnose the registry.
|
|
78
97
|
- `WENDKEEP_CONTEXT_CONFLICT`: another active context occupies the target; use another
|
|
79
98
|
branch/worktree or close the competing context correctly.
|
|
80
99
|
- `WENDKEEP_CONTEXT_GIT`: fix the branch, conflicting dirty state, or Git error and retry.
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## Objetivo
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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`
|
|
13
|
-
|
|
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,
|
|
18
|
-
|
|
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,21 @@ 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
|
+
|
|
71
85
|
## Erros comuns e diagnóstico
|
|
72
86
|
|
|
73
87
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
|
|
74
|
-
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH
|
|
75
|
-
|
|
88
|
+
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH`: a candidata escolhida não prova o checkout/HEAD atual; rode
|
|
89
|
+
`context status` novamente e selecione apenas uma candidata com `matches_actual: true`.
|
|
90
|
+
- `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: a sessão não está em quarentena ou uma candidata está ausente.
|
|
91
|
+
- `WENDKEEP_CONTEXT_CAS_MISMATCH`: a revisão mudou; descarte a decisão antiga e repita o status.
|
|
92
|
+
- `WENDKEEP_CONTEXT_IDENTITY_CHANGED`: as candidatas pertencem a identidades causais diferentes;
|
|
93
|
+
preserve a quarentena e diagnostique o registry.
|
|
76
94
|
- `WENDKEEP_CONTEXT_CONFLICT`: outro contexto ativo ocupa o destino; use outra branch/worktree ou
|
|
77
95
|
encerre corretamente o contexto concorrente.
|
|
78
96
|
- `WENDKEEP_CONTEXT_GIT`: corrija a branch, dirty state conflitante ou erro do Git e repita.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.3",
|
|
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": [
|
|
@@ -59,6 +59,9 @@ Usage:
|
|
|
59
59
|
Managed linked worktrees under .worktrees (branch default wk/<slug>).
|
|
60
60
|
wendkeep context switch <branch> [--create] [--session <id>] [--json]
|
|
61
61
|
Switch Git branch and the causal session scope in the same worktree.
|
|
62
|
+
wendkeep context status --session <id> [--json]
|
|
63
|
+
wendkeep context recover --session <id> --select reserved|observed --revision <n> --reason <text> [--json]
|
|
64
|
+
Inspect or explicitly recover a quarantined causal scope conflict.
|
|
62
65
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
63
66
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
64
67
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
package/src/context.mjs
CHANGED
|
@@ -10,16 +10,23 @@ import {
|
|
|
10
10
|
concurrentScopeConflicts,
|
|
11
11
|
scopeForRegistry,
|
|
12
12
|
} from '../hooks/project-scope.mjs';
|
|
13
|
+
import { readVaultMarker } from './project-vault.mjs';
|
|
14
|
+
import { sanitizeMemoryText } from '../packages/vault/src/memory-schema.mjs';
|
|
13
15
|
|
|
14
16
|
export const CONTEXT_HELP = `wendkeep context <subcommand>
|
|
15
17
|
|
|
16
18
|
switch <branch> [--create] [--session <id>] [--project <path>] [--vault <path>] [--json]
|
|
19
|
+
status --session <id> [--project <path>] [--vault <path>] [--json]
|
|
20
|
+
recover --session <id> --select <reserved|observed> --revision <n> --reason <text>
|
|
21
|
+
[--project <path>] [--vault <path>] [--json]
|
|
17
22
|
|
|
18
23
|
Switches Git branch and the causal session scope together inside the same worktree.
|
|
19
24
|
Without --session, exactly one active session must match the current scope.
|
|
25
|
+
Status inventories reserved/observed recovery candidates without selecting one.
|
|
26
|
+
Recover resolves a quarantined conflict only when the selected candidate still matches the checkout.
|
|
20
27
|
`;
|
|
21
28
|
|
|
22
|
-
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
|
|
29
|
+
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session', '--select', '--revision', '--reason']);
|
|
23
30
|
const FLAG_OPTIONS = new Set(['--create', '--json']);
|
|
24
31
|
|
|
25
32
|
function contextError(code, message) {
|
|
@@ -121,6 +128,247 @@ function contextRevision(entry) {
|
|
|
121
128
|
? entry.context_revision : 0;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
function activeSessionEntry(registry, sessionId) {
|
|
132
|
+
const id = String(sessionId || '').trim();
|
|
133
|
+
if (!id) throw contextError('WENDKEEP_CONTEXT_SESSION', 'status requer --session <id>');
|
|
134
|
+
const entry = registry.sessions?.[id];
|
|
135
|
+
if (!entry || entry.status !== 'active') {
|
|
136
|
+
throw contextError('WENDKEEP_CONTEXT_SESSION', `sessão ativa não encontrada: ${id}`);
|
|
137
|
+
}
|
|
138
|
+
return { id, entry };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validCandidateScope(scope, label) {
|
|
142
|
+
const required = [
|
|
143
|
+
'projectId', 'projectRoot', 'repoRoot', 'remote', 'branch', 'worktree',
|
|
144
|
+
'head', 'provider', 'sessionId',
|
|
145
|
+
];
|
|
146
|
+
const complete = scope && typeof scope === 'object' && scope.complete === true
|
|
147
|
+
&& required.every((field) => typeof scope[field] === 'string' && scope[field].trim());
|
|
148
|
+
const head = complete ? scope.head.trim() : '';
|
|
149
|
+
const branch = complete ? scope.branch.trim() : '';
|
|
150
|
+
const safeHead = /^[0-9a-f]{40,64}$/i.test(head);
|
|
151
|
+
const safeBranch = branch === `detached:${head}` || (
|
|
152
|
+
branch.length <= 255
|
|
153
|
+
&& !branch.startsWith('/')
|
|
154
|
+
&& !branch.endsWith('/')
|
|
155
|
+
&& !branch.endsWith('.')
|
|
156
|
+
&& !branch.endsWith('.lock')
|
|
157
|
+
&& !branch.includes('..')
|
|
158
|
+
&& !branch.includes('@{')
|
|
159
|
+
&& !branch.includes('//')
|
|
160
|
+
&& !/[\u0000-\u0020~^:?*[\]\\]/u.test(branch)
|
|
161
|
+
);
|
|
162
|
+
if (!complete || !safeHead || !safeBranch) {
|
|
163
|
+
throw contextError('WENDKEEP_CONTEXT_SCOPE_CONFLICT', `scope ${label} ausente ou incompleta`);
|
|
164
|
+
}
|
|
165
|
+
return scope;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function scopeMatchesActual(candidate, actual) {
|
|
169
|
+
return compareProjectScopes(candidate, actual).ok
|
|
170
|
+
&& Boolean(candidate.head)
|
|
171
|
+
&& candidate.head === actual.head;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function candidateSummary(id, candidate, actual) {
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
branch: candidate.branch || '',
|
|
178
|
+
head: candidate.head || '',
|
|
179
|
+
complete: candidate.complete === true,
|
|
180
|
+
matches_actual: scopeMatchesActual(candidate, actual),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function requiredRevision(value) {
|
|
185
|
+
const raw = String(value ?? '').trim();
|
|
186
|
+
const revision = Number(raw);
|
|
187
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(revision)) {
|
|
188
|
+
throw contextError('WENDKEEP_CONTEXT_ARGS', 'recover requer --revision <inteiro não negativo>');
|
|
189
|
+
}
|
|
190
|
+
return revision;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function recoveryReason(value) {
|
|
194
|
+
const raw = String(value || '').replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
195
|
+
if (!raw) throw contextError('WENDKEEP_CONTEXT_ARGS', 'recover requer --reason <texto>');
|
|
196
|
+
if (raw.length > 240) throw contextError('WENDKEEP_CONTEXT_ARGS', '--reason excede 240 caracteres');
|
|
197
|
+
return sanitizeMemoryText(raw).replace(/\s+/g, ' ').trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function recoverySelection(value) {
|
|
201
|
+
const selected = String(value || '').trim();
|
|
202
|
+
if (!['reserved', 'observed'].includes(selected)) {
|
|
203
|
+
throw contextError('WENDKEEP_CONTEXT_ARGS', 'recover requer --select <reserved|observed>');
|
|
204
|
+
}
|
|
205
|
+
return selected;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function scopeIdentityMismatches(reserved, observed) {
|
|
209
|
+
const fields = ['projectId', 'remote', 'provider', 'sessionId'];
|
|
210
|
+
return fields.filter((field) => String(reserved?.[field] || '') !== String(observed?.[field] || ''));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateRecoveryIdentity(vaultBase, entry, sessionId, reserved, observed = null) {
|
|
214
|
+
const mismatches = observed ? scopeIdentityMismatches(reserved, observed) : [];
|
|
215
|
+
const candidates = observed ? [reserved, observed] : [reserved];
|
|
216
|
+
if (candidates.some((candidate) => candidate.sessionId !== sessionId)) mismatches.push('sessionId');
|
|
217
|
+
if (entry.provider && candidates.some((candidate) => candidate.provider !== entry.provider)) {
|
|
218
|
+
mismatches.push('provider');
|
|
219
|
+
}
|
|
220
|
+
let marker = null;
|
|
221
|
+
try { marker = readVaultMarker(vaultBase)?.marker || null; } catch { /* fail closed below */ }
|
|
222
|
+
if (!marker?.projectId || candidates.some((candidate) => candidate.projectId !== marker.projectId)) {
|
|
223
|
+
mismatches.push('projectId');
|
|
224
|
+
}
|
|
225
|
+
if (mismatches.length) {
|
|
226
|
+
throw contextError(
|
|
227
|
+
'WENDKEEP_CONTEXT_IDENTITY_CHANGED',
|
|
228
|
+
`candidatas divergem na identidade causal (${[...new Set(mismatches)].join(', ')})`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function recoveryActualScope(projectRoot, expected, sessionId, spawn) {
|
|
234
|
+
return captureProjectScope({
|
|
235
|
+
input: { cwd: projectRoot },
|
|
236
|
+
projectRoot,
|
|
237
|
+
projectId: expected.projectId,
|
|
238
|
+
provider: expected.provider,
|
|
239
|
+
sessionId,
|
|
240
|
+
targetCwd: projectRoot,
|
|
241
|
+
spawn,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const CONFLICT_FIELDS = new Set([
|
|
246
|
+
'scope.projectId', 'scope.projectRoot', 'scope.repoRoot', 'scope.remote', 'scope.branch',
|
|
247
|
+
'scope.worktree', 'scope.provider', 'scope.sessionId', 'scope.incomplete',
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
function sanitizedConflictFields(value) {
|
|
251
|
+
if (!Array.isArray(value) || value.some((field) => !CONFLICT_FIELDS.has(String(field)))) {
|
|
252
|
+
throw contextError('WENDKEEP_CONTEXT_SCOPE_CONFLICT', 'campos de conflito ausentes ou inválidos');
|
|
253
|
+
}
|
|
254
|
+
return [...new Set(value.map(String))].sort();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function receiptScope(candidate) {
|
|
258
|
+
return { branch: candidate.branch || '', head: candidate.head || '' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function inspectSessionContext({
|
|
262
|
+
vaultBase,
|
|
263
|
+
projectRoot = process.cwd(),
|
|
264
|
+
sessionId = '',
|
|
265
|
+
spawn = spawnSync,
|
|
266
|
+
} = {}) {
|
|
267
|
+
const registry = readSessionRegistry(vaultBase);
|
|
268
|
+
const selected = activeSessionEntry(registry, sessionId);
|
|
269
|
+
const reserved = validCandidateScope(selected.entry.project_scope, 'reserved');
|
|
270
|
+
const conflict = selected.entry.project_scope_conflict === true;
|
|
271
|
+
const observed = conflict
|
|
272
|
+
? validCandidateScope(selected.entry.project_scope_observed, 'observed')
|
|
273
|
+
: null;
|
|
274
|
+
validateRecoveryIdentity(vaultBase, selected.entry, selected.id, reserved, observed);
|
|
275
|
+
const actual = recoveryActualScope(projectRoot, reserved, selected.id, spawn);
|
|
276
|
+
const candidates = [candidateSummary('reserved', reserved, actual)];
|
|
277
|
+
if (observed) candidates.push(candidateSummary('observed', observed, actual));
|
|
278
|
+
return {
|
|
279
|
+
status: conflict ? 'conflict' : 'healthy',
|
|
280
|
+
session_id: selected.id,
|
|
281
|
+
revision: contextRevision(selected.entry),
|
|
282
|
+
conflict,
|
|
283
|
+
conflict_fields: conflict ? sanitizedConflictFields(selected.entry.project_scope_conflict_fields) : [],
|
|
284
|
+
candidates,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function recoverSessionContext({
|
|
289
|
+
vaultBase,
|
|
290
|
+
projectRoot = process.cwd(),
|
|
291
|
+
sessionId = '',
|
|
292
|
+
select = '',
|
|
293
|
+
revision,
|
|
294
|
+
reason = '',
|
|
295
|
+
spawn = spawnSync,
|
|
296
|
+
mutateRegistry = mutateSessionRegistry,
|
|
297
|
+
now = () => new Date(),
|
|
298
|
+
} = {}) {
|
|
299
|
+
const requestedSessionId = String(sessionId || '').trim();
|
|
300
|
+
if (!requestedSessionId) throw contextError('WENDKEEP_CONTEXT_SESSION', 'recover requer --session <id>');
|
|
301
|
+
const selectedId = recoverySelection(select);
|
|
302
|
+
const expectedRevision = requiredRevision(revision);
|
|
303
|
+
const safeReason = recoveryReason(reason);
|
|
304
|
+
|
|
305
|
+
return mutateRegistry(vaultBase, (registry) => {
|
|
306
|
+
const selected = activeSessionEntry(registry, requestedSessionId);
|
|
307
|
+
const entry = selected.entry;
|
|
308
|
+
if (entry.project_scope_conflict !== true) {
|
|
309
|
+
throw contextError('WENDKEEP_CONTEXT_SCOPE_CONFLICT', 'a sessão não possui conflito de scope ativo');
|
|
310
|
+
}
|
|
311
|
+
const currentRevision = contextRevision(entry);
|
|
312
|
+
if (currentRevision !== expectedRevision) {
|
|
313
|
+
throw contextError(
|
|
314
|
+
'WENDKEEP_CONTEXT_CAS_MISMATCH',
|
|
315
|
+
`context_revision mudou de ${expectedRevision} para ${currentRevision}; inspecione o status novamente`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const reserved = validCandidateScope(entry.project_scope, 'reserved');
|
|
319
|
+
const observed = validCandidateScope(entry.project_scope_observed, 'observed');
|
|
320
|
+
validateRecoveryIdentity(vaultBase, entry, selected.id, reserved, observed);
|
|
321
|
+
const candidate = selectedId === 'reserved' ? reserved : observed;
|
|
322
|
+
const actual = recoveryActualScope(projectRoot, candidate, selected.id, spawn);
|
|
323
|
+
if (!scopeMatchesActual(candidate, actual)) {
|
|
324
|
+
throw contextError(
|
|
325
|
+
'WENDKEEP_CONTEXT_SCOPE_MISMATCH',
|
|
326
|
+
`a candidata ${selectedId} não corresponde integralmente ao checkout atual`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const nextRevision = currentRevision + 1;
|
|
331
|
+
const at = now().toISOString();
|
|
332
|
+
const receipt = {
|
|
333
|
+
revision: nextRevision,
|
|
334
|
+
operation: 'recover',
|
|
335
|
+
selected: selectedId,
|
|
336
|
+
from: {
|
|
337
|
+
reserved: receiptScope(reserved),
|
|
338
|
+
observed: receiptScope(observed),
|
|
339
|
+
},
|
|
340
|
+
to: receiptScope(actual),
|
|
341
|
+
actor: { provider: candidate.provider || entry.provider || '', session_id: selected.id },
|
|
342
|
+
reason: safeReason,
|
|
343
|
+
at,
|
|
344
|
+
};
|
|
345
|
+
const {
|
|
346
|
+
project_scope_conflict: _conflict,
|
|
347
|
+
project_scope_conflict_fields: _conflictFields,
|
|
348
|
+
project_scope_observed: _observed,
|
|
349
|
+
...preserved
|
|
350
|
+
} = entry;
|
|
351
|
+
registry.sessions[selected.id] = {
|
|
352
|
+
...preserved,
|
|
353
|
+
project_scope: scopeForRegistry(actual, { authorizedActions: reserved.authorizedActions }),
|
|
354
|
+
context_revision: nextRevision,
|
|
355
|
+
context_recoveries: [
|
|
356
|
+
...(Array.isArray(entry.context_recoveries) ? entry.context_recoveries : []),
|
|
357
|
+
receipt,
|
|
358
|
+
],
|
|
359
|
+
last_seen: at,
|
|
360
|
+
updated_at: at,
|
|
361
|
+
};
|
|
362
|
+
return {
|
|
363
|
+
status: 'recovered',
|
|
364
|
+
session_id: selected.id,
|
|
365
|
+
selected: selectedId,
|
|
366
|
+
revision: nextRevision,
|
|
367
|
+
receipt,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
124
372
|
function resolveSessionId(vaultBase, projectRoot, requested, spawn) {
|
|
125
373
|
const registry = readSessionRegistry(vaultBase);
|
|
126
374
|
if (requested) {
|
|
@@ -271,6 +519,12 @@ export function switchSessionContext({
|
|
|
271
519
|
|
|
272
520
|
function output(result, json) {
|
|
273
521
|
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
522
|
+
else if (Array.isArray(result.candidates)) {
|
|
523
|
+
process.stdout.write(`context ${result.status}: session ${result.session_id}; revision ${result.revision}; candidates ${result.candidates.map((candidate) => candidate.id).join(', ')}\n`);
|
|
524
|
+
}
|
|
525
|
+
else if (result.status === 'recovered') {
|
|
526
|
+
process.stdout.write(`context recovered: ${result.selected} selected (session ${result.session_id}; revision ${result.revision})\n`);
|
|
527
|
+
}
|
|
274
528
|
else process.stdout.write(`context ${result.status}: ${result.branch} (session ${result.session_id}; revision ${result.revision})\n`);
|
|
275
529
|
}
|
|
276
530
|
|
|
@@ -278,6 +532,27 @@ export function runContext(argv = []) {
|
|
|
278
532
|
try {
|
|
279
533
|
validateArgv(argv);
|
|
280
534
|
const [sub, branch, ...extra] = positionals(argv);
|
|
535
|
+
if (sub === 'status' && !branch && !extra.length) {
|
|
536
|
+
const result = inspectSessionContext({
|
|
537
|
+
vaultBase: vaultOf(argv),
|
|
538
|
+
projectRoot: projectOf(argv),
|
|
539
|
+
sessionId: optionValue(argv, '--session'),
|
|
540
|
+
});
|
|
541
|
+
output(result, argv.includes('--json'));
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
if (sub === 'recover' && !branch && !extra.length) {
|
|
545
|
+
const result = recoverSessionContext({
|
|
546
|
+
vaultBase: vaultOf(argv),
|
|
547
|
+
projectRoot: projectOf(argv),
|
|
548
|
+
sessionId: optionValue(argv, '--session'),
|
|
549
|
+
select: optionValue(argv, '--select'),
|
|
550
|
+
revision: optionValue(argv, '--revision'),
|
|
551
|
+
reason: optionValue(argv, '--reason'),
|
|
552
|
+
});
|
|
553
|
+
output(result, argv.includes('--json'));
|
|
554
|
+
return 0;
|
|
555
|
+
}
|
|
281
556
|
if (sub !== 'switch' || !branch || extra.length) {
|
|
282
557
|
throw contextError('WENDKEEP_CONTEXT_ARGS', 'use: wendkeep context switch <branch> [--create] [--session <id>]');
|
|
283
558
|
}
|