wendkeep 0.76.7 → 0.76.9
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 +27 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/docs/en/commands/context.md +35 -4
- package/docs/pt-BR/commands/context.md +34 -4
- package/hooks/active-context-handoff-evidence.mjs +6 -7
- package/hooks/active-context-store.mjs +7 -1
- package/hooks/brain-inject.mjs +30 -8
- package/hooks/change-context.mjs +9 -3
- package/hooks/change-core.mjs +4 -3
- package/package.json +3 -3
- package/packages/cli/src/index.mjs +2 -0
- package/src/active-context-health.mjs +355 -0
- package/src/active-context-runtime.mjs +12 -2
- package/src/context.mjs +20 -1
- package/src/doctor.mjs +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.9] — 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Injeção de change causal.** `brain-inject` e `change-context` usam o active context da sessão
|
|
12
|
+
para marcar `ATUAL`, calcular o hash da sentinela e preservar o mesmo foco em todos os degraus do
|
|
13
|
+
budget; o backlog continua global sem promover a change irmã pelo pointer legado.
|
|
14
|
+
- **Store vazio fail-closed.** A presença de `active_contexts`, schema ou revision — inclusive
|
|
15
|
+
`active_contexts: {}` — impede fallback a `CURRENT_CHANGE.md` quando o contexto causal está ausente
|
|
16
|
+
ou ambíguo; compatibilidade legada permanece somente antes da inicialização contextual.
|
|
17
|
+
- **Criação contextual preservada.** Operações mutáveis como `delivery start` continuam recebendo a
|
|
18
|
+
identidade causal antes de existir um context record, enquanto hooks read-only exigem o registro
|
|
19
|
+
existente e nunca publicam foco de sibling.
|
|
20
|
+
|
|
21
|
+
## [0.76.8] — 2026-08-22
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Doctor de active contexts.** A seção `[active-contexts]` cruza store, sessões e topologia Git
|
|
26
|
+
read-only para classificar sessão órfã, worktree removida e lease `request-stop` expirada; uma
|
|
27
|
+
topologia não provada permanece diagnóstico não acionável, nunca falso positivo de remoção.
|
|
28
|
+
- **Reparo explícito com CAS.** `context repair` exige key, revision, sessão ator e motivo, revalida
|
|
29
|
+
sob o lock e falha sem writes quando o alvo ficou saudável, stale ou não pode ser provado.
|
|
30
|
+
- **Preservação histórica.** Orphan/removed muda para `closed` sem apagar o contexto; lease vencida
|
|
31
|
+
isolada muda para `expired` sem fechar contexto saudável. Receipt append-only e projeções legadas
|
|
32
|
+
são atualizados sem tocar ledger, evidência, notas ou memória histórica.
|
|
33
|
+
|
|
7
34
|
## [0.76.7] — 2026-08-22
|
|
8
35
|
|
|
9
36
|
### Fixed
|
package/README.en.md
CHANGED
|
@@ -115,7 +115,8 @@ 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
|
+
Once `active_contexts` is initialized, `brain-inject` and `change-context` mark only the causal context's change as current; the backlog remains global, and an empty or ambiguous store never revives `CURRENT_CHANGE.md`.
|
|
119
120
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
121
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
122
|
- **`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`.
|
|
@@ -582,7 +583,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
|
|
|
582
583
|
(Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
|
|
583
584
|
```
|
|
584
585
|
|
|
585
|
-
The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the
|
|
586
|
+
The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the causal current-change marker, project lessons, and a `<wk_process>` router. The inventory remains global, but `CURRENT` and the sentinel hash come from the session's active context; `active_contexts: {}` fails closed instead of reviving `CURRENT_CHANGE.md`. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
|
|
586
587
|
|
|
587
588
|
The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
|
|
588
589
|
|
package/README.md
CHANGED
|
@@ -115,7 +115,8 @@ 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
|
+
Once `active_contexts` is initialized, `brain-inject` and `change-context` mark only the causal context's change as current; the backlog remains global, and an empty or ambiguous store never revives `CURRENT_CHANGE.md`.
|
|
119
120
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
121
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
122
|
- **`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`.
|
|
@@ -582,7 +583,7 @@ agent session ──hooks──▶ wendkeep ──▶ Markdown in vault ──
|
|
|
582
583
|
(Claude/Codex) (Node) (02-Sessões/…) (CORE+SHARED, ledger, backlinks)
|
|
583
584
|
```
|
|
584
585
|
|
|
585
|
-
The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the
|
|
586
|
+
The agent's settings.json points each hook at `npx wendkeep hook …`; **in Claude Code** the change-lifecycle hooks instead run the installed script directly (`node` on `${CLAUDE_PROJECT_DIR}/node_modules/wendkeep/hooks/<name>.mjs`) when the package is present locally, skipping an npx resolve on every event. `.codex/hooks.json` mirrors the same groups with PascalCase event keys, but always uses the `npx` form (`${CLAUDE_PROJECT_DIR}` does not exist in Codex) and spells its timeout `timeoutSec` — a plain `timeout` is neither a field nor an error there, it silently falls through to a 600s default, so `init` migrates that legacy key in place. On `Stop`, wendkeep parses the session transcript, appends the turn, updates token/cost data, emits derived notes, and publishes the sanitized handoff to the memory outbox. On `SessionStart` — startup, `/clear`, and `/compact` — `brain-inject` injects CORE + SHARED, every open change with its pending tasks, the causal current-change marker, project lessons, and a `<wk_process>` router. The inventory remains global, but `CURRENT` and the sentinel hash come from the session's active context; `active_contexts: {}` fails closed instead of reviving `CURRENT_CHANGE.md`. Claude, Codex, or another agent can therefore resume work started elsewhere without hiding the rest of the backlog.
|
|
586
587
|
|
|
587
588
|
The archive **gate** blocks unless: the change scaffold is filled (G0), no task is open (G1), every declared critical sensor is green (with fresh evidence), and a `verdict.json` is present and current. `--force` waives G1 only — G0 is inescapable by design (a placeholder change forced through once minted a fake ADR), and no flag turns a red sensor or a missing verdict green. The agent is instructed never to use it on its own.
|
|
588
589
|
|
|
@@ -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
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -101,6 +117,14 @@ unambiguous active context. With zero or multiple contexts it stays empty. Migra
|
|
|
101
117
|
and never invents a worktree or session identity. The legacy pointer becomes a context only
|
|
102
118
|
when one active session, a complete scope, and worktree metadata prove one identity.
|
|
103
119
|
|
|
120
|
+
The `brain-inject` (`SessionStart`) and `change-context` (`UserPromptSubmit`) hooks resolve the same
|
|
121
|
+
causal identity before marking a change as `CURRENT` or computing the sentinel hash. The backlog
|
|
122
|
+
remains global and lists every other change as `OPEN`, but a divergent `CURRENT_CHANGE.md` never
|
|
123
|
+
turns a sibling change into the session focus. The presence of `active_contexts`, its schema, or its
|
|
124
|
+
revision — including `active_contexts: {}` — disables legacy fallback: a missing or ambiguous
|
|
125
|
+
context fails closed without reviving the pointer. Fallback exists only before contextual-store
|
|
126
|
+
initialization.
|
|
127
|
+
|
|
104
128
|
## Common errors and diagnosis
|
|
105
129
|
|
|
106
130
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
|
|
@@ -116,6 +140,13 @@ when one active session, a complete scope, and worktree metadata prove one ident
|
|
|
116
140
|
- `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git and registry state and diagnose manually before
|
|
117
141
|
any new mutation.
|
|
118
142
|
- `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: replace the raw Git command with `wendkeep context switch`.
|
|
143
|
+
- `WENDKEEP_ACTIVE_CONTEXT_CAS_MISMATCH`: the target revision changed; rerun `doctor`.
|
|
144
|
+
- `WENDKEEP_ACTIVE_CONTEXT_HEALTHY`: the condition disappeared or was never repairable; do not force it.
|
|
145
|
+
- `WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN`: Git/registry could not prove worktrees; repair the
|
|
146
|
+
topology before any context repair.
|
|
147
|
+
- `WENDKEEP_ACTIVE_CONTEXT_ACTOR_MISMATCH`: the actor session does not belong to the target's proven project.
|
|
148
|
+
- `WENDKEEP_ACTIVE_CONTEXT_SESSION_ORPHAN`, `WENDKEEP_ACTIVE_CONTEXT_WORKTREE_REMOVED`, and
|
|
149
|
+
`WENDKEEP_ACTIVE_CONTEXT_LEASE_EXPIRED`: read-only diagnostics emitted by `doctor`.
|
|
119
150
|
|
|
120
151
|
## Next steps
|
|
121
152
|
|
|
@@ -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
|
|
8
|
-
|
|
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
|
|
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
|
|
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 é
|
|
@@ -98,6 +114,13 @@ contexto ativo inequívoco. Com zero ou múltiplos contextos, fica vazio. A migr
|
|
|
98
114
|
não inventa uma identidade de worktree ou sessão. O ponteiro legado só vira contexto quando uma sessão
|
|
99
115
|
ativa, scope completa e metadados da worktree provam uma única identidade.
|
|
100
116
|
|
|
117
|
+
Os hooks `brain-inject` (`SessionStart`) e `change-context` (`UserPromptSubmit`) resolvem a mesma
|
|
118
|
+
identidade causal antes de marcar uma change como `ATUAL` ou calcular o hash da sentinela. O backlog
|
|
119
|
+
continua global e lista as demais changes como `ABERTA`, mas um `CURRENT_CHANGE.md` divergente nunca
|
|
120
|
+
transforma a change irmã em foco da sessão. A presença de `active_contexts`, schema ou revision —
|
|
121
|
+
inclusive `active_contexts: {}` — desativa o fallback legado: contexto ausente ou ambíguo falha
|
|
122
|
+
fechado sem reativar o ponteiro. O fallback só existe antes da inicialização do store contextual.
|
|
123
|
+
|
|
101
124
|
## Erros comuns e diagnóstico
|
|
102
125
|
|
|
103
126
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
|
|
@@ -113,6 +136,13 @@ ativa, scope completa e metadados da worktree provam uma única identidade.
|
|
|
113
136
|
- `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git e registry e faça diagnóstico manual antes de
|
|
114
137
|
qualquer nova mutação.
|
|
115
138
|
- `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: troque o comando Git cru por `wendkeep context switch`.
|
|
139
|
+
- `WENDKEEP_ACTIVE_CONTEXT_CAS_MISMATCH`: a revision do alvo mudou; rode o `doctor` novamente.
|
|
140
|
+
- `WENDKEEP_ACTIVE_CONTEXT_HEALTHY`: a condição desapareceu ou o alvo nunca foi reparável; não force.
|
|
141
|
+
- `WENDKEEP_ACTIVE_CONTEXT_TOPOLOGY_UNPROVEN`: o Git/registry não provou as worktrees; corrija a
|
|
142
|
+
topologia antes de qualquer repair.
|
|
143
|
+
- `WENDKEEP_ACTIVE_CONTEXT_ACTOR_MISMATCH`: a sessão ator não pertence ao projeto provado do alvo.
|
|
144
|
+
- `WENDKEEP_ACTIVE_CONTEXT_SESSION_ORPHAN`, `WENDKEEP_ACTIVE_CONTEXT_WORKTREE_REMOVED` e
|
|
145
|
+
`WENDKEEP_ACTIVE_CONTEXT_LEASE_EXPIRED`: diagnósticos read-only emitidos pelo `doctor`.
|
|
116
146
|
|
|
117
147
|
## Próximos passos
|
|
118
148
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { readSessionRegistry } from './obsidian-common.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
activeContextRegistryInitialized,
|
|
4
|
+
resolveActiveContext,
|
|
5
|
+
} from './active-context-store.mjs';
|
|
3
6
|
import { resolveRuntimeActiveContext } from '../src/active-context-runtime.mjs';
|
|
4
7
|
|
|
8
|
+
export { activeContextRegistryInitialized } from './active-context-store.mjs';
|
|
9
|
+
|
|
5
10
|
const IDENTITY_FIELDS = Object.freeze([
|
|
6
11
|
['work_session_id', 'workSessionId'],
|
|
7
12
|
['repository_id', 'repositoryId'],
|
|
@@ -26,12 +31,6 @@ function contextsOf(registry) {
|
|
|
26
31
|
? registry.active_contexts : {};
|
|
27
32
|
}
|
|
28
33
|
|
|
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
34
|
function assertAuthority(identity, context) {
|
|
36
35
|
if (!identity || !context || context.state !== 'active') {
|
|
37
36
|
throw contextError(
|
|
@@ -11,6 +11,12 @@ const ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
|
|
|
11
11
|
const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
|
|
12
12
|
const DELIVERY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,100}$/;
|
|
13
13
|
|
|
14
|
+
export function activeContextRegistryInitialized(registry = {}) {
|
|
15
|
+
return Object.hasOwn(registry, 'active_contexts')
|
|
16
|
+
|| Object.hasOwn(registry, 'active_contexts_schema')
|
|
17
|
+
|| Object.hasOwn(registry, 'active_contexts_revision');
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
function contextError(code, message) {
|
|
15
21
|
const error = new Error(message);
|
|
16
22
|
error.code = code;
|
|
@@ -254,7 +260,7 @@ export function migrateLegacyActiveContext(vaultBase, {
|
|
|
254
260
|
const slug = legacyPointer(vaultBase);
|
|
255
261
|
if (!slug) return { migrated: false, reason: 'no-pointer' };
|
|
256
262
|
const registry = readSessionRegistry(vaultBase);
|
|
257
|
-
if (
|
|
263
|
+
if (activeContextRegistryInitialized(registry)) return { migrated: false, reason: 'already-initialized' };
|
|
258
264
|
if (typeof identityForSession !== 'function') return { migrated: false, reason: 'identity-unavailable' };
|
|
259
265
|
|
|
260
266
|
const candidates = [];
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -7,7 +7,12 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
9
9
|
import { brainDir } from './brain-core.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
activeChange,
|
|
12
|
+
buildActiveChangeInjection,
|
|
13
|
+
changeCtxState,
|
|
14
|
+
writeSentinel,
|
|
15
|
+
} from './change-core.mjs';
|
|
11
16
|
import { buildLessonsInjection } from './lessons-core.mjs';
|
|
12
17
|
import { getLocale } from './locale.mjs';
|
|
13
18
|
import { resolveSessionEntry } from './session-identity.mjs';
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
} from './operating-profile-runtime.mjs';
|
|
21
26
|
import { assertVaultPathSafe } from './vault-path-safety.mjs';
|
|
22
27
|
import { CORE_LIMITS, validateCore } from '../src/validate-core.mjs';
|
|
28
|
+
import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
|
|
23
29
|
|
|
24
30
|
// The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
|
|
25
31
|
// standing instruction the model plans in chat, leaves the change scaffold raw and forces the
|
|
@@ -220,7 +226,9 @@ function budgetNotice(priority, layer, message) {
|
|
|
220
226
|
return `<wk_budget_notice priority="${priority}" layer="${layer}">${message}</wk_budget_notice>`;
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
export function buildInjection(vaultBase, input = {}, {
|
|
229
|
+
export function buildInjection(vaultBase, input = {}, {
|
|
230
|
+
profile = 'GOVERN', bindingError = null, context = null,
|
|
231
|
+
} = {}) {
|
|
224
232
|
const brain = detectMemoryMode(vaultBase).mode === 'v2'
|
|
225
233
|
? buildV2Memory(vaultBase)
|
|
226
234
|
: buildLegacyMemory(vaultBase);
|
|
@@ -236,10 +244,14 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
|
|
|
236
244
|
// visible below, but the standing change router would contradict that contract.
|
|
237
245
|
const router = policy.requiresChange ? processRouter(getLocale(vaultBase).id) : '';
|
|
238
246
|
const { identity, entry } = resolveSessionEntry(vaultBase, input);
|
|
239
|
-
const
|
|
240
|
-
|
|
247
|
+
const contextualChange = context ? activeChange(vaultBase, { context }) : '';
|
|
248
|
+
const sessionChange = contextualChange || (!context && identity.state === 'resolved' ? entry?.change_slug : '');
|
|
249
|
+
const focus = sessionChange
|
|
250
|
+
? `<session_change>${boundAncillaryText(`Change vinculada a esta sessão: ${sessionChange}. Este vínculo prevalece para writes automáticos; todas as pendências continuam visíveis acima.`, '<session_change></session_change>'.length)}</session_change>`
|
|
241
251
|
: '';
|
|
242
|
-
const allChanges = buildActiveChangeInjection(vaultBase, {
|
|
252
|
+
const allChanges = buildActiveChangeInjection(vaultBase, {
|
|
253
|
+
context, maxLineChars: INJECTION_LIMITS.lineChars,
|
|
254
|
+
});
|
|
243
255
|
|
|
244
256
|
// Global priority is deterministic: memory/router/focus, then changes, then lessons.
|
|
245
257
|
let output = joinInjection([brain, profileNotice, router, focus, allChanges, lessons]);
|
|
@@ -253,6 +265,7 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
|
|
|
253
265
|
// Second pressure step: non-current changes leave the hot context before the current one.
|
|
254
266
|
const nonCurrentEvicted = budgetNotice(2, 'non-current-changes', 'Changes não atuais omitidas depois das lessons.');
|
|
255
267
|
const currentChange = buildActiveChangeInjection(vaultBase, {
|
|
268
|
+
context,
|
|
256
269
|
currentOnly: true,
|
|
257
270
|
maxLineChars: INJECTION_LIMITS.lineChars,
|
|
258
271
|
});
|
|
@@ -264,6 +277,7 @@ export function buildInjection(vaultBase, input = {}, { profile = 'GOVERN', bind
|
|
|
264
277
|
const fixed = joinInjection([brain, profileNotice, router, focus, lessonsEvicted, nonCurrentEvicted, currentSummarized]);
|
|
265
278
|
const remaining = Math.max(512, INJECTION_LIMITS.totalBytes - byteLength(fixed) - 1);
|
|
266
279
|
const boundedCurrent = buildActiveChangeInjection(vaultBase, {
|
|
280
|
+
context,
|
|
267
281
|
currentOnly: true,
|
|
268
282
|
maxBytes: remaining,
|
|
269
283
|
maxLineChars: INJECTION_LIMITS.lineChars,
|
|
@@ -285,21 +299,29 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
285
299
|
const input = readHookInput();
|
|
286
300
|
const runtime = resolveHookOperatingProfile({ input });
|
|
287
301
|
const vaultBase = runtime.vaultBase;
|
|
302
|
+
const { identity } = runtime;
|
|
303
|
+
const sid = identity.state === 'resolved'
|
|
304
|
+
? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
|
|
305
|
+
const commandContext = runtime.bindingError ? null : resolveCommandActiveContext({
|
|
306
|
+
vaultBase,
|
|
307
|
+
projectRoot: input.cwd || process.cwd(),
|
|
308
|
+
sessionId: sid,
|
|
309
|
+
requireExisting: true,
|
|
310
|
+
});
|
|
288
311
|
writeHookOutput({
|
|
289
312
|
hookSpecificOutput: {
|
|
290
313
|
hookEventName: 'SessionStart',
|
|
291
314
|
additionalContext: buildInjection(vaultBase, input, {
|
|
292
315
|
profile: runtime.profile,
|
|
293
316
|
bindingError: runtime.bindingError,
|
|
317
|
+
context: commandContext,
|
|
294
318
|
}),
|
|
295
319
|
},
|
|
296
320
|
});
|
|
297
321
|
// Sentinela do change-context: o backlog completo acabou de ser injetado aqui, então o hook
|
|
298
322
|
// UserPromptSubmit não precisa re-pingar no 1º prompt. Bônus — nunca derruba a injeção.
|
|
299
323
|
if (runtime.policy.harness && !runtime.bindingError) try {
|
|
300
|
-
const st = changeCtxState(vaultBase);
|
|
301
|
-
const { identity } = runtime;
|
|
302
|
-
const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
|
|
324
|
+
const st = changeCtxState(vaultBase, { context: commandContext });
|
|
303
325
|
if (st) {
|
|
304
326
|
writeSentinel(vaultBase, 'ctx', profileSentinelId(sid, runtime.profile), st.hash);
|
|
305
327
|
}
|
package/hooks/change-context.mjs
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
10
10
|
import { profileRuntimeError } from './brain-inject.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
activeChange, changeCtxState, readSentinel, renderOpenChanges, writeSentinel,
|
|
13
|
+
} from './change-core.mjs';
|
|
12
14
|
import {
|
|
13
15
|
hookProfilePolicy,
|
|
14
16
|
profileSentinelId,
|
|
@@ -43,7 +45,7 @@ export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug =
|
|
|
43
45
|
hash,
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
|
-
const st = changeCtxState(vaultBase);
|
|
48
|
+
const st = changeCtxState(vaultBase, { context });
|
|
47
49
|
if (st) {
|
|
48
50
|
if (readSentinel(vaultBase, 'ctx', sentinelId) === st.hash) return null;
|
|
49
51
|
writeSentinel(vaultBase, 'ctx', sentinelId, st.hash);
|
|
@@ -75,10 +77,14 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
75
77
|
vaultBase,
|
|
76
78
|
projectRoot: input.cwd || process.cwd(),
|
|
77
79
|
sessionId: sid,
|
|
80
|
+
requireExisting: true,
|
|
78
81
|
});
|
|
82
|
+
const changeSlug = commandContext
|
|
83
|
+
? activeChange(vaultBase, { context: commandContext })
|
|
84
|
+
: (entry?.change_slug || '');
|
|
79
85
|
const ping = runtime.bindingError
|
|
80
86
|
? { context: profileRuntimeError(runtime.bindingError) }
|
|
81
|
-
: buildChangePing(vaultBase, sid, input.prompt || '',
|
|
87
|
+
: buildChangePing(vaultBase, sid, input.prompt || '', changeSlug, {
|
|
82
88
|
profile: runtime.profile,
|
|
83
89
|
context: commandContext,
|
|
84
90
|
});
|
package/hooks/change-core.mjs
CHANGED
|
@@ -420,7 +420,8 @@ export function renderOpenChanges(state, {
|
|
|
420
420
|
// Mantém o nome exportado para consumidores internos existentes, mas agora injeta o backlog
|
|
421
421
|
// completo em vez de ocultar changes não selecionadas.
|
|
422
422
|
export function buildActiveChangeInjection(vaultBase, options = {}) {
|
|
423
|
-
|
|
423
|
+
const { context, ...renderOptions } = options;
|
|
424
|
+
return renderOpenChanges(allChangesState(vaultBase, { context }), renderOptions);
|
|
424
425
|
}
|
|
425
426
|
|
|
426
427
|
export function activeChangeLink(vaultBase, { context } = {}) {
|
|
@@ -479,8 +480,8 @@ export function writeSentinel(vaultBase, kind, sid, value = '1') {
|
|
|
479
480
|
// Estado global usado pelo change-context: hash cobre qualquer tarefa aberta, inclusive de uma
|
|
480
481
|
// change que não esteja no ponteiro. As propriedades slug/openTasks preservam compatibilidade com
|
|
481
482
|
// consumidores antigos e descrevem somente a atual.
|
|
482
|
-
export function changeCtxState(vaultBase) {
|
|
483
|
-
const state = allChangesState(vaultBase);
|
|
483
|
+
export function changeCtxState(vaultBase, { context } = {}) {
|
|
484
|
+
const state = allChangesState(vaultBase, { context });
|
|
484
485
|
if (!state.changes.length && !state.pointerWarning) return null;
|
|
485
486
|
const selected = state.changes.find((change) => change.current);
|
|
486
487
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.9",
|
|
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/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",
|
|
44
|
+
"precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-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/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
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
activeContextRegistryInitialized,
|
|
4
|
+
migrateLegacyActiveContext,
|
|
5
|
+
resolveActiveContext,
|
|
6
|
+
} from '../hooks/active-context-store.mjs';
|
|
3
7
|
import { captureProjectScope, compareProjectScopes } from '../hooks/project-scope.mjs';
|
|
4
8
|
import {
|
|
5
9
|
discoverWorktreeRepository,
|
|
@@ -107,6 +111,7 @@ export function resolveCommandActiveContext({
|
|
|
107
111
|
projectRoot = process.cwd(),
|
|
108
112
|
sessionId = '',
|
|
109
113
|
spawn,
|
|
114
|
+
requireExisting = false,
|
|
110
115
|
} = {}) {
|
|
111
116
|
const requestedSession = String(sessionId || '').trim();
|
|
112
117
|
let identity;
|
|
@@ -116,7 +121,7 @@ export function resolveCommandActiveContext({
|
|
|
116
121
|
});
|
|
117
122
|
} catch (error) {
|
|
118
123
|
const registry = readSessionRegistry(vaultBase);
|
|
119
|
-
const initialized =
|
|
124
|
+
const initialized = activeContextRegistryInitialized(registry);
|
|
120
125
|
const legacyUnavailable = error?.code === 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE'
|
|
121
126
|
|| (!requestedSession && error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND');
|
|
122
127
|
if (!initialized && legacyUnavailable) return null;
|
|
@@ -127,5 +132,10 @@ export function resolveCommandActiveContext({
|
|
|
127
132
|
vaultBase, projectRoot, sessionId: candidateId, ...(spawn ? { spawn } : {}),
|
|
128
133
|
}),
|
|
129
134
|
});
|
|
135
|
+
if (requireExisting) {
|
|
136
|
+
const registry = readSessionRegistry(vaultBase);
|
|
137
|
+
if (!activeContextRegistryInitialized(registry)) return null;
|
|
138
|
+
resolveActiveContext(vaultBase, identity);
|
|
139
|
+
}
|
|
130
140
|
return identity;
|
|
131
141
|
}
|
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
|