wendkeep 0.76.6 → 0.76.8

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,34 @@ 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.8] — 2026-08-22
8
+
9
+ ### Added
10
+
11
+ - **Doctor de active contexts.** A seção `[active-contexts]` cruza store, sessões e topologia Git
12
+ read-only para classificar sessão órfã, worktree removida e lease `request-stop` expirada; uma
13
+ topologia não provada permanece diagnóstico não acionável, nunca falso positivo de remoção.
14
+ - **Reparo explícito com CAS.** `context repair` exige key, revision, sessão ator e motivo, revalida
15
+ sob o lock e falha sem writes quando o alvo ficou saudável, stale ou não pode ser provado.
16
+ - **Preservação histórica.** Orphan/removed muda para `closed` sem apagar o contexto; lease vencida
17
+ isolada muda para `expired` sem fechar contexto saudável. Receipt append-only e projeções legadas
18
+ são atualizados sem tocar ledger, evidência, notas ou memória histórica.
19
+
20
+ ## [0.76.7] — 2026-08-22
21
+
22
+ ### Fixed
23
+
24
+ - **Handoff causal no Stop.** Com `active_contexts` inicializado, work session, repository,
25
+ worktree, branch e change vêm do contexto chamador; payload divergente falha antes de CAS,
26
+ nota, outbox ou ledger e não publica estado parcial.
27
+ - **Evidência da change correta.** O lifecycle coleta verdict, sensores e ADR apenas da change do
28
+ active context causal, sem reutilizar `change_slug` legado ou link global de uma sessão irmã.
29
+ - **Recall automático escopado.** `UserPromptSubmit` permanece somente leitura, sem migrar pointer
30
+ ou registry; exclui evidência de sessão/change irmã ativa e preserva material global/histórico.
31
+ `/brain-recall` explícito segue global.
32
+ - **Compatibilidade conservadora.** O comportamento anterior permanece somente quando o registry
33
+ ainda não possui nenhum campo do store contextual, inclusive quando `active_contexts` está vazio.
34
+
7
35
  ## [0.76.6] — 2026-08-22
8
36
 
9
37
  ### 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. 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**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. `doctor` diagnoses orphaned active contexts, removed worktrees, and expired `request-stop` leases without writing; `context repair --key <key> --revision <n> --reason <text> --session <id>` revalidates under lock, closes only the ownerless/removed context or expires only its lease, while preserving the record and all historical memory. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
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`.
@@ -386,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
386
386
  - **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
387
387
  - **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
388
388
  - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
389
- - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
390
- - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity; `UserPromptSubmit` injects only relevant passages with provenance.
389
+ - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
390
+ - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
391
391
 
392
392
  Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
393
393
 
package/README.md CHANGED
@@ -115,7 +115,7 @@ npx wendkeep init
115
115
 
116
116
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
117
117
  2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
118
- 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. `doctor` diagnoses orphaned active contexts, removed worktrees, and expired `request-stop` leases without writing; `context repair --key <key> --revision <n> --reason <text> --session <id>` revalidates under lock, closes only the ownerless/removed context or expires only its lease, while preserving the record and all historical memory. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
119
  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`.
@@ -386,8 +386,8 @@ Hot memory now separates human authorship, operational state, and evidence:
386
386
  - **`SHARED_MEMORY.md` is bounded generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Admission prioritizes critical operational state and never publishes beyond 48 lines/6 KiB; omitted events remain in the append-only authority and surface only as verifiable counts. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
387
387
  - **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
388
388
  - **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
389
- - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically.
390
- - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity; `UserPromptSubmit` injects only relevant passages with provenance.
389
+ - **Registers are scoped.** `git.local-head`, handoffs, verdicts, and change status carry project, work-session, change, branch, or worktree scope. Two branches do not create a global conflict; only events in the same scope and causal lineage may advance automatically. Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`, `worktree_id`, branch, and change from the causal active context; a divergent handoff fails before CAS, note, outbox, or ledger mutation. Legacy compatibility exists only without the contextual store.
390
+ - **`EVIDENCE_INDEX.jsonl` is local recall.** Markdown is chunked by headings and blocks without requiring the Observer. Ranking combines BM25, exact phrases, field weights, authority, validity, bounded recency, and source diversity. The automatic `UserPromptSubmit` hook is read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes passages owned by an active sibling session or change while preserving global and historical evidence. Explicit `/brain-recall` remains global.
391
391
 
392
392
  Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
393
393
 
@@ -4,19 +4,23 @@
4
4
 
5
5
  ## Purpose
6
6
 
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.
7
+ Inspect and move the same session's causal scope with proof of the current checkout, recover a
8
+ quarantined divergence, or explicitly repair operational authority that `doctor` proved orphaned,
9
+ bound to a removed worktree, or carrying an expired lease.
9
10
 
10
11
  ## When to use
11
12
 
12
13
  Use `context switch` to create or select another branch in the same worktree. If the registry
13
14
  already records `project_scope_conflict`, use `context status` to inventory `reserved` and
14
15
  `observed` without local paths; recover only through `context recover` and an explicit human choice.
16
+ Use `doctor` to obtain the key/revision for `active_contexts` debt; run `context repair` only after
17
+ reviewing that diagnosis and providing an active actor session and explicit reason.
15
18
 
16
19
  ## When not to use
17
20
 
18
21
  Do not use it to move to another worktree, hand-edit the registry, or replace `worktree create`.
19
22
  Recovery never selects a candidate automatically or accepts a scope that no longer matches HEAD.
23
+ `context repair` is not a healthy-context close command, an age-based cleanup, or history deletion.
20
24
 
21
25
  ## Prerequisites
22
26
 
@@ -30,6 +34,7 @@ Recovery never selects a candidate automatically or accepts a scope that no long
30
34
  npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <root>] [--vault <vault>] [--json]
31
35
  npx --no-install wendkeep context status --session <id> [--project <root>] [--vault <vault>] [--json]
32
36
  npx --no-install wendkeep context recover --session <id> --select <reserved|observed> --revision <n> --reason <text> [--project <root>] [--vault <vault>] [--json]
37
+ npx --no-install wendkeep context repair --key <repository:worktree:work-session> --revision <n> --reason <text> --session <id> [--project <root>] [--vault <vault>] [--json]
33
38
  ```
34
39
 
35
40
  Without `--session`, exactly one active session must fully match the current scope. `--create`
@@ -38,9 +43,11 @@ uses `git switch -c`; without it, the command follows `git switch` semantics.
38
43
  ## Options and exit codes
39
44
 
40
45
  - `--create`: create the branch from the current HEAD.
41
- - `--session <id>`: select the causal session explicitly; recommended whenever selection is unclear.
46
+ - `--session <id>`: select the causal session or, for repair, the active auditable actor session.
47
+ - `--key <repository:worktree:work-session>`: select exactly the diagnosed active context.
42
48
  - `--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.
49
+ - `--revision <n>`: CAS against the revision returned by `context status` or `doctor`; required for
50
+ recovery and repair.
44
51
  - `--reason <text>`: auditable reason, sanitized and limited to 240 characters.
45
52
  - `--project <root>` and `--vault <vault>`: select the binding and paths for manual use.
46
53
  - `--json`: emit status, session id, branch, HEAD, revision, and event without exposing the Vault.
@@ -57,6 +64,7 @@ npx --no-install wendkeep context switch main --session 019abc-session-id
57
64
  npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
58
65
  npx --no-install wendkeep context status --session 019abc-session-id --json
59
66
  npx --no-install wendkeep context recover --session 019abc-session-id --select observed --revision 7 --reason "checkout confirmed"
67
+ npx --no-install wendkeep context repair --key "repo:tree:work" --revision 4 --reason "worktree removed after merge" --session 019abc-session-id
60
68
  ```
61
69
 
62
70
  Do not replace it with the raw command below while the harness is active:
@@ -85,6 +93,14 @@ preserves the change/lease/authorizations, clears only the quarantine, and appen
85
93
  receipt to `context_recoveries`. Any failure leaves the registry and quarantine byte-identical.
86
94
  This is fail-closed: no candidate, receipt, or partial scope is published after a failed check.
87
95
 
96
+ For repair, the command rereads the target under lock, validates revision and actor session, proves
97
+ the Git topology again, and reapplies the diagnosis. A context without an active session or with a
98
+ removed worktree becomes `state=closed`, while its record and historical bindings remain; an
99
+ isolated expired lease becomes `expired` and the context stays active. The append-only
100
+ `active_context_repairs` receipt records actor, reason, diagnostics, and effect. Only after the
101
+ authoritative write are `CURRENT_CHANGE.md` and `CURRENT_DELIVERY` reprojected; the ledger,
102
+ evidence index, notes, and historical memory remain unchanged.
103
+
88
104
  ### Multi-context change registry
89
105
 
90
106
  `SESSION_REGISTRY.json` keeps `active_contexts` with its own schema and revision. Each entry is
@@ -116,6 +132,13 @@ when one active session, a complete scope, and worktree metadata prove one ident
116
132
  - `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git and registry state and diagnose manually before
117
133
  any new mutation.
118
134
  - `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: replace the raw Git command with `wendkeep context switch`.
135
+ - `WENDKEEP_ACTIVE_CONTEXT_CAS_MISMATCH`: the target revision changed; rerun `doctor`.
136
+ - `WENDKEEP_ACTIVE_CONTEXT_HEALTHY`: the condition disappeared or was never repairable; do not force it.
137
+ - `WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN`: Git/registry could not prove worktrees; repair the
138
+ topology before any context repair.
139
+ - `WENDKEEP_ACTIVE_CONTEXT_ACTOR_MISMATCH`: the actor session does not belong to the target's proven project.
140
+ - `WENDKEEP_ACTIVE_CONTEXT_SESSION_ORPHAN`, `WENDKEEP_ACTIVE_CONTEXT_WORKTREE_REMOVED`, and
141
+ `WENDKEEP_ACTIVE_CONTEXT_LEASE_EXPIRED`: read-only diagnostics emitted by `doctor`.
119
142
 
120
143
  ## Next steps
121
144
 
@@ -115,10 +115,17 @@ npx wendkeep validate-memory --vault <v2-vault>
115
115
  `change.<slug>.status` compete only inside the same scope. Automatic resolution still requires
116
116
  the same project and causal lineage; incompatible decisions, constraints, and blockers remain
117
117
  human-curated. An ambiguous key is omitted from SHARED without removing CORE or independent keys.
118
+ Once `active_contexts` is initialized, `Stop` derives `work_session_id`, `repository_id`,
119
+ `worktree_id`, branch, and `change_slug` from the causal active context. A divergent handoff
120
+ identity fails before CAS, note, outbox, or ledger mutation; legacy fallback exists only without
121
+ the contextual store.
118
122
  - `.brain/EVIDENCE_INDEX.jsonl` chunks documents by heading and block and records file, heading,
119
123
  type, change, session, work session, authority, date, validity, and hash. `/brain-recall` and the
120
124
  `UserPromptSubmit` hook use BM25, exact phrase, field weights, authority, validity, bounded
121
- recency, and diversity to return the matching passage with provenance.
125
+ recency, and diversity to return the matching passage with provenance. Automatic recall is
126
+ read-only: it never migrates `CURRENT_CHANGE.md` or mutates the registry; it excludes rows owned
127
+ by an active sibling session or change while preserving global or historical rows with no active
128
+ owner. Explicit `/brain-recall` remains global.
122
129
  - Every memory path validates the physical topology of `.brain`, ledger, outbox, CORE, SHARED,
123
130
  candidates, registry, notes, backups, temporary files, and sidecars before reading or writing.
124
131
  Junctions, symlinks, reparse points, or hardlinks fail closed without touching external bytes.
@@ -4,19 +4,23 @@
4
4
 
5
5
  ## Objetivo
6
6
 
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 colocada em quarentena.
7
+ Inspecionar e mover a scope causal da mesma sessão com prova do checkout atual, recuperar uma
8
+ divergência em quarentena ou reparar explicitamente autoridade operacional que o `doctor` provou
9
+ estar órfã, ligada a worktree removida ou com lease expirada.
9
10
 
10
11
  ## Quando usar
11
12
 
12
13
  Use `context switch` para criar ou selecionar outra branch na mesma worktree. Se o registry já
13
14
  registrou `project_scope_conflict`, use `context status` para inventariar `reserved` e `observed`
14
15
  sem paths locais; recupere somente com `context recover` e uma seleção humana explícita.
16
+ Use `doctor` para obter key/revision de dívida em `active_contexts`; execute `context repair` somente
17
+ depois de revisar o diagnóstico e fornecer sessão ator e motivo explícitos.
15
18
 
16
19
  ## Quando não usar
17
20
 
18
21
  Não use para mudar de worktree, editar o registry à mão ou substituir `worktree create`. Recovery
19
22
  não escolhe a candidata automaticamente e não aceita uma scope que deixou de corresponder ao HEAD.
23
+ `context repair` não serve para encerrar contexto saudável, limpar por idade ou apagar histórico.
20
24
 
21
25
  ## Pré-requisitos
22
26
 
@@ -30,6 +34,7 @@ não escolhe a candidata automaticamente e não aceita uma scope que deixou de c
30
34
  npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <raiz>] [--vault <cofre>] [--json]
31
35
  npx --no-install wendkeep context status --session <id> [--project <raiz>] [--vault <cofre>] [--json]
32
36
  npx --no-install wendkeep context recover --session <id> --select <reserved|observed> --revision <n> --reason <texto> [--project <raiz>] [--vault <cofre>] [--json]
37
+ npx --no-install wendkeep context repair --key <repository:worktree:work-session> --revision <n> --reason <texto> --session <id> [--project <raiz>] [--vault <cofre>] [--json]
33
38
  ```
34
39
 
35
40
  Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à scope atual. Use
@@ -38,9 +43,11 @@ Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à
38
43
  ## Opções e códigos de saída
39
44
 
40
45
  - `--create`: cria a branch a partir do HEAD atual.
41
- - `--session <id>`: seleciona explicitamente a sessão causal; recomendado quando houver dúvida.
46
+ - `--session <id>`: seleciona a sessão causal ou, no repair, a sessão ator ativa e auditável.
47
+ - `--key <repository:worktree:work-session>`: seleciona exatamente o active context diagnosticado.
42
48
  - `--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.
49
+ - `--revision <n>`: CAS contra a revisão exibida por `context status` ou pelo `doctor`; obrigatório
50
+ em recovery e repair.
44
51
  - `--reason <texto>`: justificativa auditável, sanitizada e limitada a 240 caracteres.
45
52
  - `--project <raiz>` e `--vault <cofre>`: selecionam binding e paths para uso manual.
46
53
  - `--json`: emite status, session id, branch, HEAD, revisão e evento sem expor o Vault.
@@ -56,6 +63,7 @@ npx --no-install wendkeep context switch main --session 019abc-session-id
56
63
  npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
57
64
  npx --no-install wendkeep context status --session 019abc-session-id --json
58
65
  npx --no-install wendkeep context recover --session 019abc-session-id --select observed --revision 7 --reason "checkout confirmado"
66
+ npx --no-install wendkeep context repair --key "repo:tree:work" --revision 4 --reason "worktree removida após merge" --session 019abc-session-id
59
67
  ```
60
68
 
61
69
  Não substitua pelo comando cru abaixo quando o harness estiver ativo:
@@ -82,6 +90,14 @@ atuais. Sob o lock do registry, o comando revalida a revisão, incrementa `conte
82
90
  change/lease/autorizações, limpa somente a quarentena e anexa um receipt sanitizado em
83
91
  `context_recoveries`. Qualquer falha deixa registry e quarentena byte a byte intactos.
84
92
 
93
+ No repair, o comando relê o alvo sob o lock, valida revision e sessão ator, prova novamente a
94
+ topologia Git e reaplica o diagnóstico. Contexto sem sessão ativa ou com worktree removida muda
95
+ para `state=closed`, mas seu registro e bindings históricos continuam presentes; uma lease vencida
96
+ isolada muda para `expired` e o contexto segue ativo. O receipt append-only em
97
+ `active_context_repairs` registra ator, motivo, diagnósticos e efeito. Só depois da escrita
98
+ autoritativa `CURRENT_CHANGE.md` e `CURRENT_DELIVERY` são reprojetados; ledger, evidence index,
99
+ notas e memória histórica não são alterados.
100
+
85
101
  ### Registry multi-contexto de changes
86
102
 
87
103
  O `SESSION_REGISTRY.json` mantém `active_contexts` com schema e revisão próprios. Cada entrada é
@@ -113,6 +129,13 @@ ativa, scope completa e metadados da worktree provam uma única identidade.
113
129
  - `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git e registry e faça diagnóstico manual antes de
114
130
  qualquer nova mutação.
115
131
  - `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: troque o comando Git cru por `wendkeep context switch`.
132
+ - `WENDKEEP_ACTIVE_CONTEXT_CAS_MISMATCH`: a revision do alvo mudou; rode o `doctor` novamente.
133
+ - `WENDKEEP_ACTIVE_CONTEXT_HEALTHY`: a condição desapareceu ou o alvo nunca foi reparável; não force.
134
+ - `WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN`: o Git/registry não provou as worktrees; corrija a
135
+ topologia antes de qualquer repair.
136
+ - `WENDKEEP_ACTIVE_CONTEXT_ACTOR_MISMATCH`: a sessão ator não pertence ao projeto provado do alvo.
137
+ - `WENDKEEP_ACTIVE_CONTEXT_SESSION_ORPHAN`, `WENDKEEP_ACTIVE_CONTEXT_WORKTREE_REMOVED` e
138
+ `WENDKEEP_ACTIVE_CONTEXT_LEASE_EXPIRED`: diagnósticos read-only emitidos pelo `doctor`.
116
139
 
117
140
  ## Próximos passos
118
141
 
@@ -114,11 +114,17 @@ npx wendkeep validate-memory --vault <cofre-v2>
114
114
  - Registradores como `git.local-head`, `handoff.latest`, `quality.latest-*` e
115
115
  `change.<slug>.status` só competem dentro do mesmo escopo. Resolução automática ainda exige o
116
116
  mesmo projeto e linhagem causal; decisões, constraints e blockers incompatíveis permanecem sob
117
- curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes.
117
+ curadoria. Uma chave ambígua é omitida de SHARED sem remover CORE ou chaves independentes. Com
118
+ `active_contexts` inicializado, o `Stop` deriva `work_session_id`, `repository_id`, `worktree_id`,
119
+ branch e `change_slug` do active context causal. Identidade divergente no handoff falha antes de
120
+ CAS, nota, outbox ou ledger; o fallback legado só existe sem o store contextual.
118
121
  - `.brain/EVIDENCE_INDEX.jsonl` divide documentos por headings e blocos e registra arquivo,
119
122
  heading, tipo, change, sessão, work session, autoridade, data, validade e hash. `/brain-recall`
120
123
  e o hook `UserPromptSubmit` usam BM25, frase exata, pesos por campo, autoridade, validade,
121
- recência limitada e diversidade para retornar o trecho do match com proveniência.
124
+ recência limitada e diversidade para retornar o trecho do match com proveniência. No recall
125
+ automático, o hook é somente leitura: não migra `CURRENT_CHANGE.md` nem altera o registry;
126
+ exclui linhas pertencentes a sessão ou change irmã ativa e preserva linhas globais ou históricas
127
+ sem owner ativo. `/brain-recall` explícito permanece global.
122
128
  - Toda rota de memória valida a topologia física de `.brain`, ledger, outbox, CORE, SHARED,
123
129
  candidates, registry, notas, backups, temporários e sidecars antes de ler ou escrever. Junction,
124
130
  symlink, reparse point ou hardlink falham fechados sem tocar bytes externos. Locks publicam owner
@@ -0,0 +1,196 @@
1
+ import { readSessionRegistry } from './obsidian-common.mjs';
2
+ import { resolveActiveContext } from './active-context-store.mjs';
3
+ import { resolveRuntimeActiveContext } from '../src/active-context-runtime.mjs';
4
+
5
+ const IDENTITY_FIELDS = Object.freeze([
6
+ ['work_session_id', 'workSessionId'],
7
+ ['repository_id', 'repositoryId'],
8
+ ['worktree_id', 'worktreeId'],
9
+ ['branch', 'branch'],
10
+ ['change_slug', 'changeSlug'],
11
+ ]);
12
+
13
+ function contextError(code, message) {
14
+ const error = new Error(message);
15
+ error.code = code;
16
+ return error;
17
+ }
18
+
19
+ function text(value) {
20
+ return String(value ?? '').trim();
21
+ }
22
+
23
+ function contextsOf(registry) {
24
+ return registry?.active_contexts && typeof registry.active_contexts === 'object'
25
+ && !Array.isArray(registry.active_contexts)
26
+ ? registry.active_contexts : {};
27
+ }
28
+
29
+ export function activeContextRegistryInitialized(registry = {}) {
30
+ return Object.hasOwn(registry, 'active_contexts')
31
+ || Object.hasOwn(registry, 'active_contexts_schema')
32
+ || Object.hasOwn(registry, 'active_contexts_revision');
33
+ }
34
+
35
+ function assertAuthority(identity, context) {
36
+ if (!identity || !context || context.state !== 'active') {
37
+ throw contextError(
38
+ 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
39
+ 'active context causal não foi resolvido antes do Stop',
40
+ );
41
+ }
42
+ for (const [contextField, identityField] of [
43
+ ['project_id', 'projectId'],
44
+ ['repository_id', 'repositoryId'],
45
+ ['worktree_id', 'worktreeId'],
46
+ ['work_session_id', 'workSessionId'],
47
+ ]) {
48
+ if (!text(context[contextField]) || text(context[contextField]) !== text(identity[identityField])) {
49
+ throw contextError(
50
+ 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
51
+ `${contextField} do active context não corresponde à sessão causal`,
52
+ );
53
+ }
54
+ }
55
+ if (!text(context.branch) || text(context.branch) !== text(identity.branch)) {
56
+ throw contextError(
57
+ 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
58
+ 'branch do active context não corresponde à sessão causal',
59
+ );
60
+ }
61
+ }
62
+
63
+ export function resolveHandoffEvidenceAuthority(vaultBase, {
64
+ input = {},
65
+ sessionId = '',
66
+ projectRoot = input.cwd || process.cwd(),
67
+ registry = readSessionRegistry(vaultBase),
68
+ resolveCommand = resolveRuntimeActiveContext,
69
+ resolveContext = resolveActiveContext,
70
+ } = {}) {
71
+ if (!activeContextRegistryInitialized(registry)) {
72
+ return { mode: 'legacy', identity: null, context: null };
73
+ }
74
+ const identity = resolveCommand({
75
+ vaultBase,
76
+ projectRoot,
77
+ sessionId,
78
+ });
79
+ if (!identity) {
80
+ throw contextError(
81
+ 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
82
+ 'registry contextual inicializado sem identidade causal para o Stop',
83
+ );
84
+ }
85
+ const context = resolveContext(vaultBase, { ...identity, sessionId });
86
+ assertAuthority(identity, context);
87
+ return { mode: 'contextual', identity, context };
88
+ }
89
+
90
+ export function resolveReadOnlyEvidenceActiveContext({
91
+ vaultBase,
92
+ projectRoot = process.cwd(),
93
+ sessionId = '',
94
+ } = {}) {
95
+ const authority = resolveHandoffEvidenceAuthority(vaultBase, {
96
+ input: { cwd: projectRoot },
97
+ projectRoot,
98
+ sessionId,
99
+ });
100
+ return authority.mode === 'contextual' ? authority.identity : null;
101
+ }
102
+
103
+ function suppliedShared(input) {
104
+ const supplied = input?.shared || input?.handoff?.shared;
105
+ return supplied && typeof supplied === 'object' && !Array.isArray(supplied)
106
+ ? { ...supplied }
107
+ : {};
108
+ }
109
+
110
+ function suppliedIdentityValues(input, shared, snake, camel) {
111
+ const direct = input?.shared && typeof input.shared === 'object' && !Array.isArray(input.shared)
112
+ ? input.shared : {};
113
+ const nested = input?.handoff?.shared && typeof input.handoff.shared === 'object'
114
+ && !Array.isArray(input.handoff.shared)
115
+ ? input.handoff.shared : {};
116
+ return [
117
+ shared[snake], shared[camel],
118
+ direct[snake], direct[camel],
119
+ nested[snake], nested[camel],
120
+ input?.[snake], input?.[camel],
121
+ ]
122
+ .map(text)
123
+ .filter(Boolean);
124
+ }
125
+
126
+ export function buildCausalSharedHandoff({ input = {}, entry = {}, authority } = {}) {
127
+ const shared = suppliedShared(input);
128
+ if (authority?.mode !== 'contextual') {
129
+ const workSessionId = text(
130
+ shared.work_session_id
131
+ || shared.workSessionId
132
+ || input.work_session_id
133
+ || input.workSessionId
134
+ || entry?.work_session_id,
135
+ );
136
+ if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
137
+ return Object.keys(shared).length ? shared : null;
138
+ }
139
+
140
+ assertAuthority(authority.identity, authority.context);
141
+ const expected = {
142
+ workSessionId: text(authority.identity.workSessionId),
143
+ repositoryId: text(authority.identity.repositoryId),
144
+ worktreeId: text(authority.identity.worktreeId),
145
+ branch: text(authority.identity.branch || authority.context.branch),
146
+ changeSlug: text(authority.context.change_slug),
147
+ };
148
+ for (const [snake, camel] of IDENTITY_FIELDS) {
149
+ const suppliedValues = suppliedIdentityValues(input, shared, snake, camel);
150
+ if (suppliedValues.some((supplied) => supplied !== expected[camel])) {
151
+ throw contextError(
152
+ 'WENDKEEP_HANDOFF_CONTEXT_MISMATCH',
153
+ `${snake} do handoff não corresponde ao active context causal`,
154
+ );
155
+ }
156
+ delete shared[camel];
157
+ if (expected[camel]) shared[snake] = expected[camel];
158
+ else delete shared[snake];
159
+ }
160
+ return Object.keys(shared).length ? shared : null;
161
+ }
162
+
163
+ export function causalChangeSlug(entry = {}, authority = {}) {
164
+ return authority?.mode === 'contextual'
165
+ ? text(authority.context?.change_slug)
166
+ : text(entry?.change_slug);
167
+ }
168
+
169
+ export function scopeEvidenceRows(rows, { activeContext = null, registry = {} } = {}) {
170
+ if (!Array.isArray(rows) || !activeContextRegistryInitialized(registry)) return rows;
171
+
172
+ const callerWorkSession = text(activeContext?.workSessionId ?? activeContext?.work_session_id);
173
+ const sessions = registry.sessions || {};
174
+ const activeContexts = Object.values(contextsOf(registry))
175
+ .filter((context) => context?.state === 'active');
176
+ const activeChangeOwners = new Map();
177
+ for (const context of activeContexts) {
178
+ const slug = text(context.change_slug);
179
+ if (slug) activeChangeOwners.set(slug, text(context.work_session_id));
180
+ }
181
+
182
+ return rows.filter((row) => {
183
+ const sessionId = text(row?.session_id);
184
+ const rowWorkSession = text(row?.work_session_id)
185
+ || text(sessionId && sessions[sessionId]?.work_session_id);
186
+ if (sessionId && !rowWorkSession) return false;
187
+ if (rowWorkSession && rowWorkSession !== callerWorkSession) return false;
188
+ if (rowWorkSession && !callerWorkSession) return false;
189
+
190
+ const changeSlug = text(row?.change_slug);
191
+ const owner = activeChangeOwners.get(changeSlug) || '';
192
+ if (owner && owner !== callerWorkSession) return false;
193
+ if (owner && !callerWorkSession) return false;
194
+ return true;
195
+ });
196
+ }
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  // UserPromptSubmit: bounded, read-only retrieval from the local evidence index.
3
3
  import { pathToFileURL } from 'node:url';
4
- import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
4
+ import { readHookInput, readSessionRegistry, writeHookOutput } from './obsidian-common.mjs';
5
5
  import { loadEvidenceIndex, recallEvidence, renderEvidenceContext } from './evidence-recall.mjs';
6
6
  import { sanitizeMemoryText } from './memory-schema.mjs';
7
7
  import { resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
8
+ import {
9
+ resolveReadOnlyEvidenceActiveContext,
10
+ scopeEvidenceRows,
11
+ } from './active-context-handoff-evidence.mjs';
8
12
  import { isBootstrapPrompt } from '../packages/integrations/src/prompt-content.mjs';
9
13
 
10
14
  export function buildPromptEvidenceContext(vaultBase, prompt, {
11
- topK = 3, maxBytes = 3072, rows = null,
15
+ topK = 3, maxBytes = 3072, rows = null, activeContext = null, registry = null,
12
16
  } = {}) {
13
17
  const query = sanitizeMemoryText(String(prompt || '')).trim();
14
18
  if (!query || isBootstrapPrompt(query)) return '';
15
19
  const evidence = rows || loadEvidenceIndex(vaultBase);
16
- if (!evidence.length) return '';
20
+ const scoped = scopeEvidenceRows(evidence, {
21
+ activeContext,
22
+ registry: registry || readSessionRegistry(vaultBase),
23
+ });
24
+ if (!scoped.length) return '';
17
25
  return sanitizeMemoryText(renderEvidenceContext(
18
- recallEvidence(evidence, query, { topK }),
26
+ recallEvidence(scoped, query, { topK }),
19
27
  { maxBytes },
20
28
  ));
21
29
  }
@@ -23,13 +31,20 @@ export function buildPromptEvidenceContext(vaultBase, prompt, {
23
31
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
24
32
  try {
25
33
  const input = readHookInput();
26
- const runtime = resolveHookOperatingProfile({ input });
34
+ const runtime = resolveHookOperatingProfile({
35
+ input,
36
+ activeContextResolver: resolveReadOnlyEvidenceActiveContext,
37
+ });
27
38
  if (runtime.bindingError) {
28
39
  writeHookOutput({});
29
40
  } else {
30
41
  const context = buildPromptEvidenceContext(
31
42
  runtime.vaultBase,
32
43
  input.prompt || input.user_prompt || '',
44
+ {
45
+ activeContext: runtime.activeContext,
46
+ registry: readSessionRegistry(runtime.vaultBase),
47
+ },
33
48
  );
34
49
  writeHookOutput(context ? {
35
50
  hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context },
@@ -133,6 +133,7 @@ export function resolveHookOperatingProfile({
133
133
  resolution = null,
134
134
  config = null,
135
135
  provider,
136
+ activeContextResolver = resolveCommandActiveContext,
136
137
  } = {}) {
137
138
  const initialResolution = resolution || resolveProjectVault({ input });
138
139
  const matched = matchingProjectBinding(initialResolution, input);
@@ -158,7 +159,7 @@ export function resolveHookOperatingProfile({
158
159
  let contextError = null;
159
160
  if (!bindingError && identity.state === 'resolved') {
160
161
  try {
161
- activeContext = resolveCommandActiveContext({
162
+ activeContext = activeContextResolver({
162
163
  vaultBase: vaultResolution.base,
163
164
  projectRoot: input?.cwd || vaultResolution.projectRoot || process.cwd(),
164
165
  sessionId: identity.canonicalConversationId,
@@ -19,6 +19,11 @@ import { mutateSessionNote } from './session-note-io.mjs';
19
19
  import { appendIterationOutcome } from './session-iteration-outcome.mjs';
20
20
  import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
21
21
  import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
22
+ import {
23
+ buildCausalSharedHandoff,
24
+ causalChangeSlug,
25
+ resolveHandoffEvidenceAuthority,
26
+ } from './active-context-handoff-evidence.mjs';
22
27
  import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
23
28
  import { detectMemoryMode } from './memory-mode.mjs';
24
29
  import { sanitizeMemoryText } from './memory-schema.mjs';
@@ -521,21 +526,6 @@ function shouldFinalizeSession() {
521
526
  return process.env.OBSIDIAN_NO_AUTO_FINALIZE !== '1';
522
527
  }
523
528
 
524
- function sharedHandoffFromInput(input = {}, entry = {}) {
525
- const supplied = input.shared || input.handoff?.shared;
526
- const shared = supplied && typeof supplied === 'object' && !Array.isArray(supplied)
527
- ? { ...supplied }
528
- : {};
529
- const workSessionId = shared.work_session_id
530
- || shared.workSessionId
531
- || input.work_session_id
532
- || input.workSessionId
533
- || entry?.work_session_id
534
- || '';
535
- if (!shared.work_session_id && workSessionId) shared.work_session_id = workSessionId;
536
- return Object.keys(shared).length ? shared : null;
537
- }
538
-
539
529
  export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
540
530
  if (detectMemoryMode(vaultBase).mode === 'legacy') {
541
531
  return { status: 'legacy', eventCount: 0, eventIds: [], checkpoint: null };
@@ -1277,6 +1267,29 @@ export async function main({
1277
1267
  const tx = parseTranscript(identity.transcriptPath || input.transcript_path || input.transcriptPath);
1278
1268
  const requestedTurnId = String(input.turn_id || input.turnId || '');
1279
1269
  const sessionId = identity.canonicalConversationId;
1270
+ let handoffEvidenceAuthority;
1271
+ let sharedHandoff;
1272
+ let activeContextChangeSlug;
1273
+ try {
1274
+ handoffEvidenceAuthority = resolveHandoffEvidenceAuthority(vaultBase, {
1275
+ input,
1276
+ sessionId,
1277
+ projectRoot: input.cwd || process.cwd(),
1278
+ });
1279
+ sharedHandoff = buildCausalSharedHandoff({
1280
+ input,
1281
+ entry,
1282
+ authority: handoffEvidenceAuthority,
1283
+ });
1284
+ activeContextChangeSlug = causalChangeSlug(entry, handoffEvidenceAuthority);
1285
+ } catch (error) {
1286
+ const detail = String(error?.message || 'active context causal indisponível');
1287
+ process.stderr.write(`[wendkeep] Stop causal bloqueado: ${detail}\n`);
1288
+ writeHookOutput({
1289
+ systemMessage: `wendkeep: Stop bloqueado antes de publicar handoff/evidência: ${detail}`,
1290
+ });
1291
+ return;
1292
+ }
1280
1293
  const finalizing = shouldFinalizeSession();
1281
1294
  const turnIdentity = resolveTurnIdentity(tx, requestedTurnId);
1282
1295
  if (!turnIdentity) {
@@ -1389,11 +1402,10 @@ export async function main({
1389
1402
  }
1390
1403
  const finalSummary = sessionFinalSummary(tx);
1391
1404
  const memoryEvidence = collectLifecycleEvidence(vaultBase, {
1392
- changeSlug: entry.change_slug,
1405
+ changeSlug: activeContextChangeSlug,
1393
1406
  summary: finalSummary,
1394
1407
  noteRel: sessionRel,
1395
1408
  });
1396
- const sharedHandoff = sharedHandoffFromInput(input, entry);
1397
1409
  memoryHandoff = {
1398
1410
  projectId,
1399
1411
  identity,
@@ -1531,9 +1543,11 @@ export async function main({
1531
1543
  // grafo quando a change fechava antes do turno seguinte. Aqui sobrevive ao reopen e acumula toda
1532
1544
  // change que passou pela sessão (upsertListSection deduplica). Fail-quiet: nunca derruba o Stop.
1533
1545
  try {
1534
- const chgLink = entry.change_slug
1535
- ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${entry.change_slug}/proposta]]`
1536
- : activeChangeLink(vaultBase);
1546
+ const chgLink = activeContextChangeSlug
1547
+ ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${activeContextChangeSlug}/proposta]]`
1548
+ : handoffEvidenceAuthority.mode === 'legacy'
1549
+ ? activeChangeLink(vaultBase)
1550
+ : '';
1537
1551
  const wl = (chgLink.match(/\[\[[^\]]+\]\]/) || [])[0];
1538
1552
  if (wl) {
1539
1553
  mutateSessionNote(sessionPath, (cur) => (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.76.6",
3
+ "version": "0.76.8",
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,8 +41,8 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
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",
44
+ "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check packages/vault/src/worktree-metadata.mjs",
45
+ "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/active-context-health.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
46
46
  "test": "node --test --test-concurrency=2",
47
47
  "test:core": "node scripts/run-scope.mjs core",
48
48
  "release": "node scripts/release.mjs",
@@ -61,7 +61,9 @@ Usage:
61
61
  Switch Git branch and the causal session scope in the same worktree.
62
62
  wendkeep context status --session <id> [--json]
63
63
  wendkeep context recover --session <id> --select reserved|observed --revision <n> --reason <text> [--json]
64
+ wendkeep context repair --key <repository:worktree:work-session> --revision <n> --reason <text> --session <id> [--json]
64
65
  Inspect or explicitly recover a quarantined causal scope conflict.
66
+ Repair revalidates orphan/removed contexts or expired request leases without deleting history.
65
67
  wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
66
68
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
67
69
  --session <id> selects the causal active_context for implicit change operations.
@@ -0,0 +1,355 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ import {
4
+ mutateSessionRegistry,
5
+ readSessionRegistry,
6
+ } from '../hooks/obsidian-common.mjs';
7
+ import {
8
+ projectLegacyActiveChange,
9
+ projectLegacyActiveDelivery,
10
+ } from '../hooks/active-context-store.mjs';
11
+ import {
12
+ discoverWorktreeRepository,
13
+ readWorktreeRegistry,
14
+ worktreeIdentity,
15
+ } from '../packages/vault/src/worktree-metadata.mjs';
16
+ import { sanitizeMemoryText } from '../packages/vault/src/memory-schema.mjs';
17
+
18
+ export const ACTIVE_CONTEXT_DIAGNOSTICS = Object.freeze({
19
+ SESSION_ORPHAN: 'WENDKEEP_ACTIVE_CONTEXT_SESSION_ORPHAN',
20
+ WORKTREE_REMOVED: 'WENDKEEP_ACTIVE_CONTEXT_WORKTREE_REMOVED',
21
+ LEASE_EXPIRED: 'WENDKEEP_ACTIVE_CONTEXT_LEASE_EXPIRED',
22
+ TOPOLOGY_UNPROVEN: 'WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN',
23
+ IDENTITY_MISMATCH: 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH',
24
+ });
25
+
26
+ const ACTIONABLE_CODES = new Set([
27
+ ACTIVE_CONTEXT_DIAGNOSTICS.SESSION_ORPHAN,
28
+ ACTIVE_CONTEXT_DIAGNOSTICS.WORKTREE_REMOVED,
29
+ ACTIVE_CONTEXT_DIAGNOSTICS.LEASE_EXPIRED,
30
+ ]);
31
+
32
+ function healthError(code, message) {
33
+ const error = new Error(message);
34
+ error.code = code;
35
+ return error;
36
+ }
37
+
38
+ function contextualRegistryInitialized(registry) {
39
+ return Boolean(registry && (
40
+ Object.hasOwn(registry, 'active_contexts')
41
+ || Object.hasOwn(registry, 'active_contexts_schema')
42
+ || Object.hasOwn(registry, 'active_contexts_revision')
43
+ ));
44
+ }
45
+
46
+ function contextsOf(registry) {
47
+ return registry?.active_contexts && typeof registry.active_contexts === 'object'
48
+ && !Array.isArray(registry.active_contexts)
49
+ ? registry.active_contexts : {};
50
+ }
51
+
52
+ function nonNegativeInteger(value, fallback = 0) {
53
+ const parsed = Number(value);
54
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
55
+ }
56
+
57
+ function activeOwnerSessions(registry, context) {
58
+ return Object.entries(registry.sessions || {}).filter(([, entry]) => (
59
+ entry?.status === 'active'
60
+ && entry?.project_scope?.complete === true
61
+ && String(entry.project_scope.projectId || '') === String(context.project_id || '')
62
+ && String(entry.work_session_id || '') === String(context.work_session_id || '')
63
+ ));
64
+ }
65
+
66
+ function leaseExpired(registry, lease, context) {
67
+ if (!lease || lease.state !== 'active' || lease.expires_on !== 'request-stop') return false;
68
+ const owner = registry.sessions?.[String(lease.session_id || '')];
69
+ if (!owner || owner.status !== 'active'
70
+ || owner?.project_scope?.complete !== true
71
+ || String(owner.project_scope.projectId || '') !== String(context.project_id || '')) return true;
72
+ const requestedSequence = nonNegativeInteger(lease.request_turn_sequence, -1);
73
+ const stoppedSequence = nonNegativeInteger(owner.last_stop_turn_sequence, -1);
74
+ if (requestedSequence >= 0 && stoppedSequence >= requestedSequence) return true;
75
+ const requestedTurn = String(lease.request_turn_id || '');
76
+ return Boolean(requestedTurn && requestedTurn === String(owner.last_stop_turn_id || ''));
77
+ }
78
+
79
+ function issue(code, key, context, message, repairable = true) {
80
+ return {
81
+ code,
82
+ key,
83
+ revision: nonNegativeInteger(context?.revision),
84
+ repairable,
85
+ message,
86
+ };
87
+ }
88
+
89
+ export function inspectActiveContextTopology({
90
+ projectRoot = process.cwd(),
91
+ spawn = spawnSync,
92
+ } = {}) {
93
+ try {
94
+ const repository = discoverWorktreeRepository({ startDir: projectRoot, spawn });
95
+ const metadata = readWorktreeRegistry(repository).registry;
96
+ if (!metadata) {
97
+ return {
98
+ proven: false,
99
+ errorCode: 'WENDKEEP_WORKTREE_REGISTRY_MISSING',
100
+ message: 'registry de worktrees ausente',
101
+ };
102
+ }
103
+ const ids = [];
104
+ for (const worktree of repository.worktrees) {
105
+ const discovered = discoverWorktreeRepository({ startDir: worktree.path, spawn });
106
+ if (discovered.commonDir !== repository.commonDir) {
107
+ throw healthError('WENDKEEP_WORKTREE_IDENTITY_MISMATCH', 'worktree pertence a outro Git common dir');
108
+ }
109
+ ids.push(worktreeIdentity(metadata.repositoryId, discovered.gitDir));
110
+ }
111
+ return {
112
+ proven: true,
113
+ projectId: metadata.projectId,
114
+ repositoryId: metadata.repositoryId,
115
+ worktreeIds: [...new Set(ids)].sort(),
116
+ };
117
+ } catch (error) {
118
+ return {
119
+ proven: false,
120
+ errorCode: error?.code || 'WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_FAILED',
121
+ message: String(error?.message || error),
122
+ };
123
+ }
124
+ }
125
+
126
+ export function diagnoseActiveContexts({ registry = {}, topology = {} } = {}) {
127
+ const initialized = contextualRegistryInitialized(registry);
128
+ if (!initialized) {
129
+ return { initialized: false, contexts: 0, healthy: 0, topology_proven: false, issues: [] };
130
+ }
131
+
132
+ const active = Object.entries(contextsOf(registry)).filter(([, context]) => context?.state === 'active');
133
+ const issues = [];
134
+ let healthy = 0;
135
+ for (const [key, context] of active) {
136
+ const contextIssues = [];
137
+ if (!activeOwnerSessions(registry, context).length) {
138
+ contextIssues.push(issue(
139
+ ACTIVE_CONTEXT_DIAGNOSTICS.SESSION_ORPHAN,
140
+ key,
141
+ context,
142
+ 'nenhuma sessão ativa possui a work_session_id do contexto',
143
+ ));
144
+ }
145
+
146
+ if (!topology?.proven) {
147
+ contextIssues.push(issue(
148
+ ACTIVE_CONTEXT_DIAGNOSTICS.TOPOLOGY_UNPROVEN,
149
+ key,
150
+ context,
151
+ `topologia Git não provada${topology?.errorCode ? ` (${topology.errorCode})` : ''}`,
152
+ false,
153
+ ));
154
+ } else if (context.project_id !== topology.projectId || context.repository_id !== topology.repositoryId) {
155
+ contextIssues.push(issue(
156
+ ACTIVE_CONTEXT_DIAGNOSTICS.IDENTITY_MISMATCH,
157
+ key,
158
+ context,
159
+ 'contexto não pertence ao projeto/repositório provado pelo checkout',
160
+ false,
161
+ ));
162
+ } else if (!(topology.worktreeIds || []).includes(context.worktree_id)) {
163
+ contextIssues.push(issue(
164
+ ACTIVE_CONTEXT_DIAGNOSTICS.WORKTREE_REMOVED,
165
+ key,
166
+ context,
167
+ 'worktree_id não existe na topologia Git atual',
168
+ ));
169
+ }
170
+
171
+ if (leaseExpired(registry, context.operating_profile_task, context)) {
172
+ contextIssues.push(issue(
173
+ ACTIVE_CONTEXT_DIAGNOSTICS.LEASE_EXPIRED,
174
+ key,
175
+ context,
176
+ 'lease request-stop permaneceu ativa depois do término de sua requisição',
177
+ ));
178
+ }
179
+ if (!contextIssues.length) healthy += 1;
180
+ issues.push(...contextIssues);
181
+ }
182
+ return {
183
+ initialized: true,
184
+ contexts: active.length,
185
+ healthy,
186
+ topology_proven: topology?.proven === true,
187
+ issues,
188
+ };
189
+ }
190
+
191
+ export function inspectActiveContextHealth({
192
+ vaultBase,
193
+ projectRoot = process.cwd(),
194
+ spawn = spawnSync,
195
+ } = {}) {
196
+ const registry = readSessionRegistry(vaultBase);
197
+ const topology = inspectActiveContextTopology({ projectRoot, spawn });
198
+ return diagnoseActiveContexts({ registry, topology });
199
+ }
200
+
201
+ export function renderActiveContextHealthLines(result) {
202
+ if (!result.initialized) return ['[active-contexts] legado — store contextual não inicializado'];
203
+ const repairable = result.issues.filter((item) => item.repairable).length;
204
+ const lines = [
205
+ `[active-contexts] ${result.contexts} ativo(s) · ${result.healthy} saudável(is) · ${result.issues.length} diagnóstico(s) · ${repairable} reparável(is)`,
206
+ ];
207
+ for (const item of result.issues) {
208
+ lines.push(` ${item.repairable ? '→' : '!'} ${item.code}: ${item.key} @ revision ${item.revision} — ${item.message}`);
209
+ if (item.repairable) {
210
+ lines.push(` wendkeep context repair --key "${item.key}" --revision ${item.revision} --reason "<motivo>" --session <id>`);
211
+ }
212
+ }
213
+ if (!result.issues.length) lines.push(' active contexts íntegros ✓');
214
+ return lines;
215
+ }
216
+
217
+ function requiredKey(value) {
218
+ const key = String(value || '').trim();
219
+ if (!/^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$/.test(key) || key.length > 482) {
220
+ throw healthError('WENDKEEP_CONTEXT_ARGS', 'repair requer --key <repository:worktree:work-session>');
221
+ }
222
+ return key;
223
+ }
224
+
225
+ function requiredRevision(value) {
226
+ const raw = String(value ?? '').trim();
227
+ const parsed = Number(raw);
228
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(parsed)) {
229
+ throw healthError('WENDKEEP_CONTEXT_ARGS', 'repair requer --revision <inteiro não negativo>');
230
+ }
231
+ return parsed;
232
+ }
233
+
234
+ function requiredReason(value) {
235
+ const reason = sanitizeMemoryText(String(value || ''))
236
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
237
+ .replace(/\s+/g, ' ')
238
+ .trim();
239
+ if (!reason) throw healthError('WENDKEEP_CONTEXT_ARGS', 'repair requer --reason <texto>');
240
+ if (reason.length > 240) throw healthError('WENDKEEP_CONTEXT_ARGS', '--reason excede 240 caracteres');
241
+ return reason;
242
+ }
243
+
244
+ function globalRevision(registry) {
245
+ return nonNegativeInteger(registry.active_contexts_revision);
246
+ }
247
+
248
+ export function repairActiveContext({
249
+ vaultBase,
250
+ projectRoot = process.cwd(),
251
+ key,
252
+ revision,
253
+ reason,
254
+ actorSessionId,
255
+ spawn = spawnSync,
256
+ topologyProvider = (options) => inspectActiveContextTopology(options),
257
+ mutateRegistry = mutateSessionRegistry,
258
+ projectChange = projectLegacyActiveChange,
259
+ projectDelivery = projectLegacyActiveDelivery,
260
+ now = () => new Date(),
261
+ } = {}) {
262
+ const targetKey = requiredKey(key);
263
+ const expectedRevision = requiredRevision(revision);
264
+ const safeReason = requiredReason(reason);
265
+ const actorId = String(actorSessionId || '').trim();
266
+ if (!actorId) throw healthError('WENDKEEP_CONTEXT_SESSION', 'repair requer --session <id>');
267
+
268
+ const result = mutateRegistry(vaultBase, (registry) => {
269
+ if (!contextualRegistryInitialized(registry)) {
270
+ throw healthError('WENDKEEP_ACTIVE_CONTEXT_NOT_INITIALIZED', 'store contextual não inicializado');
271
+ }
272
+ const actor = registry.sessions?.[actorId];
273
+ if (!actor || actor.status !== 'active') {
274
+ throw healthError('WENDKEEP_CONTEXT_SESSION', `sessão ator ativa não encontrada: ${actorId}`);
275
+ }
276
+ const current = contextsOf(registry)[targetKey];
277
+ if (!current || current.state !== 'active') {
278
+ throw healthError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', `active context não encontrado: ${targetKey}`);
279
+ }
280
+ if (actor?.project_scope?.complete !== true
281
+ || String(actor.project_scope.projectId || '') !== String(current.project_id || '')) {
282
+ throw healthError(
283
+ 'WENDKEEP_ACTIVE_CONTEXT_ACTOR_MISMATCH',
284
+ 'sessão ator não pertence ao projeto do active context',
285
+ );
286
+ }
287
+ const currentRevision = nonNegativeInteger(current.revision);
288
+ if (currentRevision !== expectedRevision) {
289
+ throw healthError(
290
+ 'WENDKEEP_ACTIVE_CONTEXT_CAS_MISMATCH',
291
+ `active context revision mudou de ${expectedRevision} para ${currentRevision}`,
292
+ );
293
+ }
294
+ const topology = topologyProvider({ projectRoot, spawn, registry: structuredClone(registry) });
295
+ const diagnosis = diagnoseActiveContexts({ registry, topology });
296
+ const targetIssues = diagnosis.issues.filter((item) => item.key === targetKey);
297
+ if (targetIssues.some((item) => item.code === ACTIVE_CONTEXT_DIAGNOSTICS.TOPOLOGY_UNPROVEN)) {
298
+ throw healthError('WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN', 'topologia Git não pôde ser revalidada');
299
+ }
300
+ if (targetIssues.some((item) => !item.repairable)) {
301
+ throw healthError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', 'identidade do contexto não corresponde ao checkout provado');
302
+ }
303
+ const actionable = targetIssues.filter((item) => ACTIONABLE_CODES.has(item.code));
304
+ if (!actionable.length) {
305
+ throw healthError('WENDKEEP_ACTIVE_CONTEXT_HEALTHY', 'active context não possui condição reparável vigente');
306
+ }
307
+
308
+ const codes = [...new Set(actionable.map((item) => item.code))].sort();
309
+ const closeContext = codes.includes(ACTIVE_CONTEXT_DIAGNOSTICS.SESSION_ORPHAN)
310
+ || codes.includes(ACTIVE_CONTEXT_DIAGNOSTICS.WORKTREE_REMOVED);
311
+ const at = now().toISOString();
312
+ const lease = current.operating_profile_task;
313
+ const expireLease = codes.includes(ACTIVE_CONTEXT_DIAGNOSTICS.LEASE_EXPIRED)
314
+ && lease?.state === 'active';
315
+ const next = {
316
+ ...current,
317
+ ...(expireLease ? {
318
+ operating_profile_task: { ...lease, state: 'expired', expired_at: at },
319
+ } : {}),
320
+ ...(closeContext ? { state: 'closed', closed_at: at } : {}),
321
+ revision: currentRevision + 1,
322
+ updated_at: at,
323
+ };
324
+ const effect = closeContext ? 'context-closed' : 'lease-expired';
325
+ const receipt = {
326
+ operation: 'repair',
327
+ key: targetKey,
328
+ from_revision: currentRevision,
329
+ to_revision: next.revision,
330
+ diagnostics: codes,
331
+ effect,
332
+ actor: { session_id: actorId, provider: String(actor.provider || '') },
333
+ reason: safeReason,
334
+ at,
335
+ };
336
+ registry.active_contexts = { ...contextsOf(registry), [targetKey]: next };
337
+ registry.active_contexts_schema = nonNegativeInteger(registry.active_contexts_schema, 1) || 1;
338
+ registry.active_contexts_revision = globalRevision(registry) + 1;
339
+ registry.active_context_repairs = [
340
+ ...(Array.isArray(registry.active_context_repairs) ? registry.active_context_repairs : []),
341
+ receipt,
342
+ ];
343
+ return {
344
+ status: 'repaired',
345
+ key: targetKey,
346
+ revision: next.revision,
347
+ effect,
348
+ diagnostics: codes,
349
+ receipt,
350
+ };
351
+ });
352
+ projectChange(vaultBase);
353
+ projectDelivery(vaultBase);
354
+ return result;
355
+ }
package/src/context.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from '../hooks/project-scope.mjs';
13
13
  import { readVaultMarker } from './project-vault.mjs';
14
14
  import { sanitizeMemoryText } from '../packages/vault/src/memory-schema.mjs';
15
+ import { repairActiveContext } from './active-context-health.mjs';
15
16
 
16
17
  export const CONTEXT_HELP = `wendkeep context <subcommand>
17
18
 
@@ -19,14 +20,17 @@ export const CONTEXT_HELP = `wendkeep context <subcommand>
19
20
  status --session <id> [--project <path>] [--vault <path>] [--json]
20
21
  recover --session <id> --select <reserved|observed> --revision <n> --reason <text>
21
22
  [--project <path>] [--vault <path>] [--json]
23
+ repair --key <repository:worktree:work-session> --revision <n> --reason <text> --session <id>
24
+ [--project <path>] [--vault <path>] [--json]
22
25
 
23
26
  Switches Git branch and the causal session scope together inside the same worktree.
24
27
  Without --session, exactly one active session must match the current scope.
25
28
  Status inventories reserved/observed recovery candidates without selecting one.
26
29
  Recover resolves a quarantined conflict only when the selected candidate still matches the checkout.
30
+ Repair revalidates an orphan/removed context or expired request lease under CAS; it never deletes history.
27
31
  `;
28
32
 
29
- const VALUE_OPTIONS = new Set(['--project', '--vault', '--session', '--select', '--revision', '--reason']);
33
+ const VALUE_OPTIONS = new Set(['--project', '--vault', '--session', '--select', '--revision', '--reason', '--key']);
30
34
  const FLAG_OPTIONS = new Set(['--create', '--json']);
31
35
 
32
36
  function contextError(code, message) {
@@ -525,6 +529,9 @@ function output(result, json) {
525
529
  else if (result.status === 'recovered') {
526
530
  process.stdout.write(`context recovered: ${result.selected} selected (session ${result.session_id}; revision ${result.revision})\n`);
527
531
  }
532
+ else if (result.status === 'repaired') {
533
+ process.stdout.write(`context repaired: ${result.key} (${result.effect}; revision ${result.revision})\n`);
534
+ }
528
535
  else process.stdout.write(`context ${result.status}: ${result.branch} (session ${result.session_id}; revision ${result.revision})\n`);
529
536
  }
530
537
 
@@ -553,6 +560,18 @@ export function runContext(argv = []) {
553
560
  output(result, argv.includes('--json'));
554
561
  return 0;
555
562
  }
563
+ if (sub === 'repair' && !branch && !extra.length) {
564
+ const result = repairActiveContext({
565
+ vaultBase: vaultOf(argv),
566
+ projectRoot: projectOf(argv),
567
+ key: optionValue(argv, '--key'),
568
+ revision: optionValue(argv, '--revision'),
569
+ reason: optionValue(argv, '--reason'),
570
+ actorSessionId: optionValue(argv, '--session'),
571
+ });
572
+ output(result, argv.includes('--json'));
573
+ return 0;
574
+ }
556
575
  if (sub !== 'switch' || !branch || extra.length) {
557
576
  throw contextError('WENDKEEP_CONTEXT_ARGS', 'use: wendkeep context switch <branch> [--create] [--session <id>]');
558
577
  }
package/src/doctor.mjs CHANGED
@@ -7,6 +7,10 @@ import { runVaultHealth } from '../hooks/vault-health.mjs';
7
7
  import { checkSyncDefs } from './sync-defs.mjs';
8
8
  import { resolveProjectVault } from './project-vault.mjs';
9
9
  import { inspectObserverSqlOutbox } from './observer-sql-publish.mjs';
10
+ import {
11
+ inspectActiveContextHealth,
12
+ renderActiveContextHealthLines,
13
+ } from './active-context-health.mjs';
10
14
 
11
15
  const healthStatusLabel = (status) => ({
12
16
  healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
@@ -140,6 +144,9 @@ export function runDoctor(argv) {
140
144
  process.stdout.write(` → ${issue.slug}: ${issue.errorCode} — ${issue.repair}\n`);
141
145
  }
142
146
 
147
+ const activeContexts = inspectActiveContextHealth({ vaultBase, projectRoot });
148
+ process.stdout.write(`\n${renderActiveContextHealthLines(activeContexts).join('\n')}\n`);
149
+
143
150
  // 3. Link/graph health — órfãos que o grafo do Obsidian mostraria, com o comando de reparo.
144
151
  const links = checkVaultLinks(vaultBase);
145
152
  const graphLabel = links.graphColors === true ? 'com cores' : links.graphColors === false ? 'sem cores' : 'sem graph.json';
@@ -189,6 +196,7 @@ export function runDoctor(argv) {
189
196
  || repairable.length
190
197
  || warnings.length
191
198
  || worktrees.issues.length
199
+ || activeContexts.issues.length
192
200
  || links.derivedOrphans
193
201
  || links.artifactOrphans
194
202
  || links.graphColors === false