wendkeep 0.76.0 → 0.76.2
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 +28 -0
- package/README.en.md +5 -4
- package/README.md +5 -4
- package/docs/en/commands/context.md +88 -0
- package/docs/en/commands/memory.md +7 -0
- package/docs/pt-BR/commands/context.md +86 -0
- package/docs/pt-BR/commands/memory.md +7 -0
- package/hooks/project-scope.mjs +51 -6
- package/hooks/vault-health.mjs +15 -6
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +10 -0
- package/packages/vault/src/memory-schema.mjs +122 -18
- package/packages/vault/src/memory-store.mjs +4 -0
- package/packages/vault/src/validate-memory.mjs +40 -3
- package/src/context.mjs +297 -0
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.2] — 2026-08-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Projeção SHARED bounded na origem.** O projector agora admite eventos completos de forma
|
|
12
|
+
determinística e priorizada, nunca publica `SHARED_MEMORY.md` acima de 48 linhas/6144 bytes e
|
|
13
|
+
declara as contagens projetada/omitida sem perder a autoridade integral do ledger.
|
|
14
|
+
- **Omissão bounded verificável.** O gate semântico rederiva a seleção esperada; apenas o recorte
|
|
15
|
+
exato é aceito como aviso operacional, enquanto IDs ou contagens divergentes continuam bloqueando.
|
|
16
|
+
- **Recuperação antes de rescope.** Com memória estruturalmente bloqueada, o doctor orienta
|
|
17
|
+
`memory repair` e novo `memory status --gate` antes de voltar ao dry-run, sem recomendar
|
|
18
|
+
`memory rescope --apply` sobre uma projeção inválida.
|
|
19
|
+
|
|
20
|
+
## [0.76.1] — 2026-08-21
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Transição causal de branch.** `context switch <branch> [--create] [--session <id>]` move
|
|
25
|
+
Git e a scope da mesma sessão juntos na worktree atual, preserva change/lease/autorizações,
|
|
26
|
+
incrementa revisão e registra evidência `from/to` sob o lock do registry.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Guard não deixa mais a sessão inutilizável após checkout.** `git checkout/switch` cru que
|
|
31
|
+
mudaria branch é negado antecipadamente com orientação acionável; falha durante a transição
|
|
32
|
+
segura executa rollback da branch/HEAD e não publica scope parcial. Contextos incompletos de
|
|
33
|
+
outra worktree comprovada também deixam de causar falso conflito na worktree atual.
|
|
34
|
+
|
|
7
35
|
## [0.76.0] — 2026-08-21
|
|
8
36
|
|
|
9
37
|
### Added
|
package/README.en.md
CHANGED
|
@@ -115,7 +115,7 @@ npx wendkeep init
|
|
|
115
115
|
|
|
116
116
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
117
117
|
2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
|
|
118
|
-
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
118
|
+
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
119
119
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
120
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
121
|
- **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
|
|
@@ -239,6 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
|
|
|
239
239
|
|---|---|---|
|
|
240
240
|
| **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
|
|
241
241
|
| **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
|
|
242
|
+
| **Active context** | `context switch`, causal branch transition, and rollback | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
|
|
242
243
|
| **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
|
|
243
244
|
| **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
|
|
244
245
|
| **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
|
|
@@ -372,7 +373,7 @@ an installation failure.
|
|
|
372
373
|
Hot memory now separates human authorship, operational state, and evidence:
|
|
373
374
|
|
|
374
375
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
375
|
-
- **`SHARED_MEMORY.md` is 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. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
376
|
+
- **`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.
|
|
376
377
|
- **`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.
|
|
377
378
|
- **`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.
|
|
378
379
|
- **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.
|
|
@@ -388,9 +389,9 @@ Codex uses `session_id`/`turn_id` plus transcript order, with no artificial caus
|
|
|
388
389
|
|
|
389
390
|
### Injection and budgets
|
|
390
391
|
|
|
391
|
-
`CORE.md` is the only manual layer: it accepts up to 40 lines, warns from 35, keeps a 4 KiB ceiling, and caps each line at 320 characters. `SHARED_MEMORY.md` is generated exclusively by the projector and ledger; never edit it to repair state. `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; SHARED reserves up to 6 KiB. Under pressure, lessons are removed first, then non-current changes. CORE and SHARED
|
|
392
|
+
`CORE.md` is the only manual layer: it accepts up to 40 lines, warns from 35, keeps a 4 KiB ceiling, and caps each line at 320 characters. `SHARED_MEMORY.md` is generated exclusively by the projector and ledger; never edit it to repair state. `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; SHARED reserves up to 48 lines/6 KiB. Under pressure, lessons are removed first, then non-current changes. CORE is never sliced and SHARED admits deterministic whole events rather than prefix-slicing; a missing, invalid, or over-budget layer becomes a visible, repairable `<wk_memory_error>`.
|
|
392
393
|
|
|
393
|
-
`memory status --gate` and `validate-memory --vault` also check semantic coverage: they report a code, counts, and active/projected/missing keys. An empty v2 bundle is neutral; a missing
|
|
394
|
+
`memory status --gate` and `validate-memory --vault` also check semantic coverage: they report a code, counts, and active/projected/missing keys. An empty v2 bundle is neutral; a bounded omission that exactly reproduces ledger selection is an operational warning. An arbitrarily missing event, false count, placeholders as the only content, or an unresolved decision link remains explicitly degraded/blocking without printing memory values.
|
|
394
395
|
|
|
395
396
|
`DIGEST.md` is no longer the operational handoff: it remains the `/brain-recall` bridge and legacy-vault fallback. A vault without SHARED receives CORE+DIGEST with a deprecation warning; migrate during the compatibility window:
|
|
396
397
|
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ npx wendkeep init
|
|
|
115
115
|
|
|
116
116
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
117
117
|
2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
|
|
118
|
-
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
118
|
+
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
119
119
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
120
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
121
|
- **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
|
|
@@ -239,6 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
|
|
|
239
239
|
|---|---|---|
|
|
240
240
|
| **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
|
|
241
241
|
| **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
|
|
242
|
+
| **Active context** | `context switch`, causal branch transition, and rollback | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
|
|
242
243
|
| **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
|
|
243
244
|
| **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
|
|
244
245
|
| **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
|
|
@@ -372,7 +373,7 @@ an installation failure.
|
|
|
372
373
|
Hot memory now separates human authorship, operational state, and evidence:
|
|
373
374
|
|
|
374
375
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
375
|
-
- **`SHARED_MEMORY.md` is 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. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
376
|
+
- **`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.
|
|
376
377
|
- **`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.
|
|
377
378
|
- **`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.
|
|
378
379
|
- **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.
|
|
@@ -388,9 +389,9 @@ Codex uses `session_id`/`turn_id` plus transcript order, with no artificial caus
|
|
|
388
389
|
|
|
389
390
|
### Injection and budgets
|
|
390
391
|
|
|
391
|
-
`CORE.md` is the only manual layer: it accepts up to 40 lines, warns from 35, keeps a 4 KiB ceiling, and caps each line at 320 characters. `SHARED_MEMORY.md` is generated exclusively by the projector and ledger; never edit it to repair state. `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; SHARED reserves up to 6 KiB. Under pressure, lessons are removed first, then non-current changes. CORE and SHARED
|
|
392
|
+
`CORE.md` is the only manual layer: it accepts up to 40 lines, warns from 35, keeps a 4 KiB ceiling, and caps each line at 320 characters. `SHARED_MEMORY.md` is generated exclusively by the projector and ledger; never edit it to repair state. `brain-inject` delivers the same revision/hash on `startup`, `/clear`, and `/compact` `SessionStart` events, always placing CORE and SHARED before change context. The full envelope is capped at 24 KiB; SHARED reserves up to 48 lines/6 KiB. Under pressure, lessons are removed first, then non-current changes. CORE is never sliced and SHARED admits deterministic whole events rather than prefix-slicing; a missing, invalid, or over-budget layer becomes a visible, repairable `<wk_memory_error>`.
|
|
392
393
|
|
|
393
|
-
`memory status --gate` and `validate-memory --vault` also check semantic coverage: they report a code, counts, and active/projected/missing keys. An empty v2 bundle is neutral; a missing
|
|
394
|
+
`memory status --gate` and `validate-memory --vault` also check semantic coverage: they report a code, counts, and active/projected/missing keys. An empty v2 bundle is neutral; a bounded omission that exactly reproduces ledger selection is an operational warning. An arbitrarily missing event, false count, placeholders as the only content, or an unresolved decision link remains explicitly degraded/blocking without printing memory values.
|
|
394
395
|
|
|
395
396
|
`DIGEST.md` is no longer the operational handoff: it remains the `/brain-recall` bridge and legacy-vault fallback. A vault without SHARED receives CORE+DIGEST with a deprecation warning; migrate during the compatibility window:
|
|
396
397
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Active context
|
|
2
|
+
|
|
3
|
+
**English** · [Português](../../pt-BR/commands/context.md)
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Switch the Git branch and the same session's causal scope together inside the current worktree,
|
|
8
|
+
without opening a new session or weakening the guard.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Use `context switch` when an active session must create or select another branch in the same
|
|
13
|
+
worktree and continue mutating the repository after the transition.
|
|
14
|
+
|
|
15
|
+
## When not to use
|
|
16
|
+
|
|
17
|
+
Do not use it to move to another worktree, adopt an already-divergent scope, repair the registry,
|
|
18
|
+
or replace `worktree create`. Those cases require a separate physical context or explicit diagnosis.
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
- A Git project bound to a Vault through `.wendkeep.json`.
|
|
23
|
+
- An active session with a complete `project_scope` matching the current worktree.
|
|
24
|
+
- Git on `PATH` and a branch accepted by `git check-ref-format --branch`.
|
|
25
|
+
|
|
26
|
+
## Syntax
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <root>] [--vault <vault>] [--json]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Without `--session`, exactly one active session must fully match the current scope. `--create`
|
|
33
|
+
uses `git switch -c`; without it, the command follows `git switch` semantics.
|
|
34
|
+
|
|
35
|
+
## Options and exit codes
|
|
36
|
+
|
|
37
|
+
- `--create`: create the branch from the current HEAD.
|
|
38
|
+
- `--session <id>`: select the causal session explicitly; recommended whenever selection is unclear.
|
|
39
|
+
- `--project <root>` and `--vault <vault>`: select the binding and paths for manual use.
|
|
40
|
+
- `--json`: emit status, session id, branch, HEAD, revision, and event without exposing the Vault.
|
|
41
|
+
|
|
42
|
+
Exit `0` means the transition completed or the target was already active. Invalid usage,
|
|
43
|
+
ambiguity, scope mismatch, conflict, Git failure, or rollback returns `2` with a
|
|
44
|
+
`WENDKEEP_CONTEXT_*` code.
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx --no-install wendkeep context switch wk/auth --create
|
|
50
|
+
npx --no-install wendkeep context switch main --session 019abc-session-id
|
|
51
|
+
npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Do not replace it with the raw command below while the harness is active:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git switch -c wk/auth
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The guard returns `WENDKEEP_CONTEXT_SWITCH_REQUIRED` before Git runs, preventing the next mutation
|
|
61
|
+
from failing with a scope mismatch.
|
|
62
|
+
|
|
63
|
+
## Expected result
|
|
64
|
+
|
|
65
|
+
The command validates the initial scope under lock, switches branch, proves project, repository,
|
|
66
|
+
remote, worktree, provider, and session id stayed unchanged, increments `context_revision`, and
|
|
67
|
+
appends a `from/to` event to `context_transitions`. The active change, task lease, and existing
|
|
68
|
+
authorizations are preserved.
|
|
69
|
+
|
|
70
|
+
If validation or persistence fails after the switch, rollback restores the previous branch or
|
|
71
|
+
detached HEAD; a branch created by the failed attempt is removed as well.
|
|
72
|
+
|
|
73
|
+
## Common errors and diagnosis
|
|
74
|
+
|
|
75
|
+
- `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
|
|
76
|
+
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH` or `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: return to the reserved
|
|
77
|
+
checkout or diagnose the session; the command never adopts a post-hoc divergence.
|
|
78
|
+
- `WENDKEEP_CONTEXT_CONFLICT`: another active context occupies the target; use another
|
|
79
|
+
branch/worktree or close the competing context correctly.
|
|
80
|
+
- `WENDKEEP_CONTEXT_GIT`: fix the branch, conflicting dirty state, or Git error and retry.
|
|
81
|
+
- `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git and registry state and diagnose manually before
|
|
82
|
+
any new mutation.
|
|
83
|
+
- `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: replace the raw Git command with `wendkeep context switch`.
|
|
84
|
+
|
|
85
|
+
## Next steps
|
|
86
|
+
|
|
87
|
+
See [managed worktrees](worktrees.md) to create isolated checkouts and
|
|
88
|
+
[changes and verification](changes-and-verification.md) to continue the lifecycle on the new branch.
|
|
@@ -98,12 +98,19 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
98
98
|
and its successor. Replay is CORE-aware, checkpoints use the physical ledger cursor, and the
|
|
99
99
|
command neither rewrites ledger/CORE/notes nor consumes the outbox. Retrying the same applied
|
|
100
100
|
decision is idempotent.
|
|
101
|
+
- The projector deterministically admits whole events, prioritizing critical operational state up
|
|
102
|
+
to 48 lines/6144 bytes. `projection_mode`, `projected_events`, and `omitted_events` make the
|
|
103
|
+
selection verifiable; revision, cursor, and `state_hash` still cover the complete ledger. The
|
|
104
|
+
gate accepts only omissions that re-derive exactly from the same policy.
|
|
101
105
|
- `memory rescope` is a dry run by default and lists only planned IDs, keys, and scopes. With
|
|
102
106
|
`--apply`, it appends explicit project, work-session, change, branch, or worktree events while
|
|
103
107
|
preserving historic bytes as the ledger prefix. Legacy `handoff.latest` events inside candidates
|
|
104
108
|
are also rescoped individually when they carry proven session identity, separating independent
|
|
105
109
|
workflows without selecting a winner. Ambiguities that remain inside one scope stay under human
|
|
106
110
|
curation; retry returns `unchanged`.
|
|
111
|
+
If doctor reports structurally blocked memory, run `memory repair` first, confirm a green
|
|
112
|
+
`memory status --gate`, and only then return to the dry run; never apply rescope over an invalid
|
|
113
|
+
projection.
|
|
107
114
|
- Registers such as `git.local-head`, `handoff.latest`, `quality.latest-*`, and
|
|
108
115
|
`change.<slug>.status` compete only inside the same scope. Automatic resolution still requires
|
|
109
116
|
the same project and causal lineage; incompatible decisions, constraints, and blockers remain
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Contexto ativo
|
|
2
|
+
|
|
3
|
+
**PT-BR** · [English](../../en/commands/context.md)
|
|
4
|
+
|
|
5
|
+
## Objetivo
|
|
6
|
+
|
|
7
|
+
Trocar a branch Git e a scope causal da mesma sessão juntas, dentro da worktree atual, sem abrir
|
|
8
|
+
uma nova sessão e sem relaxar o guard.
|
|
9
|
+
|
|
10
|
+
## Quando usar
|
|
11
|
+
|
|
12
|
+
Use `context switch` quando uma sessão ativa precisa criar ou selecionar outra branch na mesma
|
|
13
|
+
worktree e deve continuar mutando o repositório depois da troca.
|
|
14
|
+
|
|
15
|
+
## Quando não usar
|
|
16
|
+
|
|
17
|
+
Não use para mudar de worktree, adotar uma scope já divergente, reparar o registry ou substituir
|
|
18
|
+
`worktree create`. Esses casos exigem outro contexto físico ou diagnóstico explícito.
|
|
19
|
+
|
|
20
|
+
## Pré-requisitos
|
|
21
|
+
|
|
22
|
+
- Projeto Git vinculado a um Vault por `.wendkeep.json`.
|
|
23
|
+
- Sessão ativa com `project_scope` completa e correspondente à worktree atual.
|
|
24
|
+
- Git disponível no `PATH` e branch válida segundo `git check-ref-format --branch`.
|
|
25
|
+
|
|
26
|
+
## Sintaxe
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx --no-install wendkeep context switch <branch> [--create] [--session <id>] [--project <raiz>] [--vault <cofre>] [--json]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Sem `--session`, exatamente uma sessão ativa deve corresponder integralmente à scope atual. Use
|
|
33
|
+
`--create` para executar `git switch -c`; sem a flag, a semântica é `git switch`.
|
|
34
|
+
|
|
35
|
+
## Opções e códigos de saída
|
|
36
|
+
|
|
37
|
+
- `--create`: cria a branch a partir do HEAD atual.
|
|
38
|
+
- `--session <id>`: seleciona explicitamente a sessão causal; recomendado quando houver dúvida.
|
|
39
|
+
- `--project <raiz>` e `--vault <cofre>`: selecionam binding e paths para uso manual.
|
|
40
|
+
- `--json`: emite status, session id, branch, HEAD, revisão e evento sem expor o Vault.
|
|
41
|
+
|
|
42
|
+
Exit `0` significa transição concluída ou destino já ativo. Uso inválido, ambiguidade,
|
|
43
|
+
scope divergente, conflito, falha Git ou rollback retorna `2` com um código `WENDKEEP_CONTEXT_*`.
|
|
44
|
+
|
|
45
|
+
## Exemplos
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx --no-install wendkeep context switch wk/auth --create
|
|
49
|
+
npx --no-install wendkeep context switch main --session 019abc-session-id
|
|
50
|
+
npx --no-install wendkeep context switch wk/auth --session 019abc-session-id --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Não substitua pelo comando cru abaixo quando o harness estiver ativo:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git switch -c wk/auth
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
O guard responde `WENDKEEP_CONTEXT_SWITCH_REQUIRED` antes do Git, evitando que a próxima mutação
|
|
60
|
+
falhe por mismatch.
|
|
61
|
+
|
|
62
|
+
## Resultado esperado
|
|
63
|
+
|
|
64
|
+
O comando valida a scope inicial sob lock, troca a branch, prova que projeto, repositório, remoto,
|
|
65
|
+
worktree, provider e session id não mudaram, incrementa `context_revision` e anexa um evento
|
|
66
|
+
`from/to` em `context_transitions`. Change ativa, task lease e autorizações são preservadas.
|
|
67
|
+
|
|
68
|
+
Se qualquer validação ou persistência falhar depois do switch, o rollback restaura a branch ou
|
|
69
|
+
detached HEAD anterior; uma branch criada pela tentativa também é removida.
|
|
70
|
+
|
|
71
|
+
## Erros comuns e diagnóstico
|
|
72
|
+
|
|
73
|
+
- `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
|
|
74
|
+
- `WENDKEEP_CONTEXT_SCOPE_MISMATCH` ou `WENDKEEP_CONTEXT_SCOPE_CONFLICT`: volte ao checkout
|
|
75
|
+
reservado ou diagnostique a sessão; o comando não adota uma divergência posterior.
|
|
76
|
+
- `WENDKEEP_CONTEXT_CONFLICT`: outro contexto ativo ocupa o destino; use outra branch/worktree ou
|
|
77
|
+
encerre corretamente o contexto concorrente.
|
|
78
|
+
- `WENDKEEP_CONTEXT_GIT`: corrija a branch, dirty state conflitante ou erro do Git e repita.
|
|
79
|
+
- `WENDKEEP_CONTEXT_ROLLBACK_FAILED`: preserve Git e registry e faça diagnóstico manual antes de
|
|
80
|
+
qualquer nova mutação.
|
|
81
|
+
- `WENDKEEP_CONTEXT_SWITCH_REQUIRED`: troque o comando Git cru por `wendkeep context switch`.
|
|
82
|
+
|
|
83
|
+
## Próximos passos
|
|
84
|
+
|
|
85
|
+
Veja [worktrees gerenciadas](worktrees.md) para criar checkouts isolados e
|
|
86
|
+
[changes e verificação](changes-and-verification.md) para continuar o lifecycle na nova branch.
|
|
@@ -98,12 +98,19 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
98
98
|
do attempt exato, salva backup do registry e limita a mutação ao attempt ambíguo e à sucessora.
|
|
99
99
|
O replay é CORE-aware, usa cursor físico do ledger no checkpoint e não reescreve ledger, CORE ou
|
|
100
100
|
notas, nem consome a outbox. Repetir a mesma decisão aplicada é idempotente.
|
|
101
|
+
- O projector admite eventos completos de forma determinística e prioriza estado operacional
|
|
102
|
+
crítico até o limite de 48 linhas/6144 bytes. `projection_mode`, `projected_events` e
|
|
103
|
+
`omitted_events` tornam o recorte verificável; revision, cursor e `state_hash` continuam cobrindo
|
|
104
|
+
o ledger integral. O gate aceita somente a omissão que rederiva exatamente da mesma política.
|
|
101
105
|
- `memory rescope` é dry-run por padrão e lista somente IDs, chaves e escopos planejados. Com
|
|
102
106
|
`--apply`, anexa eventos explícitos de projeto, work session, change, branch ou worktree e mantém
|
|
103
107
|
os bytes históricos como prefixo do ledger. Eventos legados de `handoff.latest` que participam
|
|
104
108
|
de candidates também são reescopados individualmente quando possuem identidade de sessão
|
|
105
109
|
comprovável: isso separa workflows independentes sem selecionar vencedor. Ambiguidades que
|
|
106
110
|
permanecem no mesmo escopo continuam sob curadoria; uma repetição retorna `unchanged`.
|
|
111
|
+
Se o doctor indicar memória estruturalmente bloqueada, execute primeiro `memory repair`, confirme
|
|
112
|
+
`memory status --gate` verde e só então volte ao dry-run; não aplique rescope sobre uma projeção
|
|
113
|
+
inválida.
|
|
107
114
|
- Registradores como `git.local-head`, `handoff.latest`, `quality.latest-*` e
|
|
108
115
|
`change.<slug>.status` só competem dentro do mesmo escopo. Resolução automática ainda exige o
|
|
109
116
|
mesmo projeto e linhagem causal; decisões, constraints e blockers incompatíveis permanecem sob
|
package/hooks/project-scope.mjs
CHANGED
|
@@ -115,11 +115,15 @@ function invocationOf(segment) {
|
|
|
115
115
|
if (executable === 'git' || executable === 'git.exe' || executable === 'git.cmd') {
|
|
116
116
|
return { kind: 'git', args: segment.slice(index + 1).map(unquote) };
|
|
117
117
|
}
|
|
118
|
+
const args = segment.slice(index + 1).map(unquote);
|
|
119
|
+
if (isWendKeepContextSwitch(executable, args)) {
|
|
120
|
+
return { kind: 'context-switch', args };
|
|
121
|
+
}
|
|
118
122
|
if (['rm', 'rm.exe', 'del', 'erase', 'remove-item', 'move-item', 'set-content', 'out-file', 'copy-item', 'new-item'].includes(executable)) {
|
|
119
|
-
return { kind: 'filesystem', args
|
|
123
|
+
return { kind: 'filesystem', args };
|
|
120
124
|
}
|
|
121
|
-
if (isPublicationInvocation(executable,
|
|
122
|
-
return { kind: 'publication', args
|
|
125
|
+
if (isPublicationInvocation(executable, args)) {
|
|
126
|
+
return { kind: 'publication', args };
|
|
123
127
|
}
|
|
124
128
|
return null;
|
|
125
129
|
}
|
|
@@ -133,6 +137,24 @@ function isWendKeepPublication(args) {
|
|
|
133
137
|
return command === 'publish' || command === 'release';
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
function isWendKeepContextSwitch(executable, args) {
|
|
141
|
+
let commandArgs = args;
|
|
142
|
+
if (['node', 'node.exe'].includes(executable)) {
|
|
143
|
+
const entrypoint = args.findIndex((arg) => executableName(arg) === 'wendkeep.mjs');
|
|
144
|
+
if (entrypoint < 0) return false;
|
|
145
|
+
commandArgs = args.slice(entrypoint + 1);
|
|
146
|
+
} else if (['npx', 'npx.cmd'].includes(executable)) {
|
|
147
|
+
const packageIndex = args.findIndex((arg) => ['wendkeep', 'wk'].includes(executableName(arg)));
|
|
148
|
+
if (packageIndex < 0) return false;
|
|
149
|
+
commandArgs = args.slice(packageIndex + 1);
|
|
150
|
+
} else if (!['wendkeep', 'wendkeep.cmd', 'wk', 'wk.cmd'].includes(executable)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const command = firstNonOption(commandArgs);
|
|
154
|
+
const commandIndex = commandArgs.findIndex((arg) => String(arg).toLowerCase() === command);
|
|
155
|
+
return command === 'context' && String(commandArgs[commandIndex + 1] || '').toLowerCase() === 'switch';
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
function isPublicationInvocation(executable, args) {
|
|
137
159
|
if (['npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'yarn', 'yarn.cmd', 'bun', 'bun.exe'].includes(executable)) {
|
|
138
160
|
const command = firstNonOption(args);
|
|
@@ -214,12 +236,27 @@ export function scopeActionsForCommand(command) {
|
|
|
214
236
|
const actions = [];
|
|
215
237
|
for (const invocation of commandInvocations(command)) {
|
|
216
238
|
if (invocation.kind === 'git') actions.push(gitAction(invocation.args));
|
|
239
|
+
if (invocation.kind === 'context-switch') actions.push('git:destructive');
|
|
217
240
|
if (invocation.kind === 'filesystem') actions.push('filesystem:mutation');
|
|
218
241
|
if (invocation.kind === 'publication') actions.push('publish');
|
|
219
242
|
}
|
|
220
243
|
return [...new Set(actions.filter(Boolean))];
|
|
221
244
|
}
|
|
222
245
|
|
|
246
|
+
export function commandChangesGitBranch(command) {
|
|
247
|
+
return commandInvocations(command).some((invocation) => {
|
|
248
|
+
if (invocation.kind !== 'git') return false;
|
|
249
|
+
const subcommand = firstGitSubcommand(invocation.args);
|
|
250
|
+
if (subcommand === 'switch') return true;
|
|
251
|
+
if (subcommand !== 'checkout') return false;
|
|
252
|
+
const subcommandIndex = invocation.args.findIndex((arg) => String(arg).toLowerCase() === 'checkout');
|
|
253
|
+
const checkoutArgs = invocation.args.slice(subcommandIndex + 1);
|
|
254
|
+
if (!checkoutArgs.length || checkoutArgs.includes('--')) return false;
|
|
255
|
+
if (checkoutArgs.includes('-p') || checkoutArgs.includes('--patch') || checkoutArgs.includes('--help')) return false;
|
|
256
|
+
return true;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
223
260
|
export function commandChangesDirectory(command) {
|
|
224
261
|
return shellSegments(command).some((segment) => {
|
|
225
262
|
const executable = executableName(segment[0]);
|
|
@@ -333,7 +370,14 @@ export function concurrentScopeConflicts(expectedScope, activeSessions = [], cur
|
|
|
333
370
|
if (!entry || String(sessionId) === String(current) || String(entry.sessionId || '') === String(current)) continue;
|
|
334
371
|
if (entry.status && entry.status !== 'active') continue;
|
|
335
372
|
const other = entry.project_scope || entry.projectScope || (entry.complete !== undefined ? entry : null);
|
|
336
|
-
if (!other
|
|
373
|
+
if (!other) {
|
|
374
|
+
conflicts.push({ sessionId: String(sessionId), reason: 'scope-unavailable' });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const leftWorktree = comparableScopeValue(expectedScope, 'worktree');
|
|
378
|
+
const rightWorktree = comparableScopeValue(other, 'worktree');
|
|
379
|
+
if (leftWorktree && rightWorktree && leftWorktree !== rightWorktree) continue;
|
|
380
|
+
if (other.complete !== true) {
|
|
337
381
|
conflicts.push({ sessionId: String(sessionId), reason: 'scope-unavailable' });
|
|
338
382
|
continue;
|
|
339
383
|
}
|
|
@@ -343,8 +387,6 @@ export function concurrentScopeConflicts(expectedScope, activeSessions = [], cur
|
|
|
343
387
|
return Boolean(left && right && left === right);
|
|
344
388
|
});
|
|
345
389
|
if (!sameRepositoryBranch) continue;
|
|
346
|
-
const leftWorktree = comparableScopeValue(expectedScope, 'worktree');
|
|
347
|
-
const rightWorktree = comparableScopeValue(other, 'worktree');
|
|
348
390
|
if (!leftWorktree || !rightWorktree || leftWorktree === rightWorktree) {
|
|
349
391
|
conflicts.push({ sessionId: String(sessionId), reason: 'same-repository-branch' });
|
|
350
392
|
}
|
|
@@ -388,6 +430,9 @@ export function scopeDecision({
|
|
|
388
430
|
if (concurrent.length) {
|
|
389
431
|
return decision(host, `WENDKEEP_SCOPE_CONFLICT: há ${concurrent.length} sessão(ões) ativa(s) com a mesma raiz Git/branch ou escopo não comprovado; use um worktree distinto ou selecione explicitamente o projeto para criar uma nova lease.`);
|
|
390
432
|
}
|
|
433
|
+
if (commandChangesGitBranch(command)) {
|
|
434
|
+
return decision(host, 'WENDKEEP_CONTEXT_SWITCH_REQUIRED: troca de branch Git crua deixaria a sessão fora da scope reservada; use `wendkeep context switch <branch> [--create] [--session <id>]`.');
|
|
435
|
+
}
|
|
391
436
|
const authorized = Array.isArray(expectedScope.authorizedActions)
|
|
392
437
|
? expectedScope.authorizedActions
|
|
393
438
|
: null;
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -523,12 +523,21 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
523
523
|
const label = historicalHandoffs.length === 1
|
|
524
524
|
? '1 handoff histórico reparável'
|
|
525
525
|
: `${historicalHandoffs.length} handoffs históricos reparáveis`;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
526
|
+
if (!bundle.ok) {
|
|
527
|
+
warnings.push(
|
|
528
|
+
`${label}; a memória está bloqueada e rescope não deve ampliar uma projeção inválida. `
|
|
529
|
+
+ `Repare primeiro: ${memoryRepairCommand(vaultBase)}. `
|
|
530
|
+
+ `Confirme a recuperação: ${memoryStatusCommand(vaultBase)}. `
|
|
531
|
+
+ 'Somente depois de um gate verde reexecute o doctor para revisar o dry-run de rescope.',
|
|
532
|
+
);
|
|
533
|
+
} else {
|
|
534
|
+
warnings.push(
|
|
535
|
+
`${label}; sessões de origem estão encerradas e nenhuma versão precisa virar estado atual. `
|
|
536
|
+
+ `Execute primeiro o dry-run: ${memoryRescopeCommand(vaultBase)}. `
|
|
537
|
+
+ `Se a prévia estiver correta: ${memoryRescopeCommand(vaultBase, { apply: true })}. `
|
|
538
|
+
+ `Dívida restante: ${memoryCurateAllCommand(vaultBase)}.`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
532
541
|
}
|
|
533
542
|
if (outbox.count) warnings.push(`${outbox.count} evento(s) pendente(s) na outbox; execute o projector quando seguro.`);
|
|
534
543
|
if (ordinaryCandidates.length) warnings.push(`${ordinaryCandidates.length} candidate(s) aguardando curadoria humana.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.2",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"precheck": "node --check src/worktree.mjs && node --check packages/vault/src/worktree-metadata.mjs",
|
|
44
|
+
"precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
|
|
45
45
|
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
46
46
|
"test": "node --test --test-concurrency=2",
|
|
47
47
|
"test:core": "node scripts/run-scope.mjs core",
|
|
@@ -57,6 +57,8 @@ Usage:
|
|
|
57
57
|
wendkeep worktree status [slug] [--json]
|
|
58
58
|
wendkeep worktree open <slug> [--editor vscode] [--json]
|
|
59
59
|
Managed linked worktrees under .worktrees (branch default wk/<slug>).
|
|
60
|
+
wendkeep context switch <branch> [--create] [--session <id>] [--json]
|
|
61
|
+
Switch Git branch and the causal session scope in the same worktree.
|
|
60
62
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
61
63
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
62
64
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
@@ -200,6 +202,9 @@ async function main(argv) {
|
|
|
200
202
|
} else if (cmd === 'profile') {
|
|
201
203
|
const { PROFILE_HELP } = await import('../../../src/profile.mjs');
|
|
202
204
|
process.stdout.write(PROFILE_HELP);
|
|
205
|
+
} else if (cmd === 'context') {
|
|
206
|
+
const { CONTEXT_HELP } = await import('../../../src/context.mjs');
|
|
207
|
+
process.stdout.write(CONTEXT_HELP);
|
|
203
208
|
} else {
|
|
204
209
|
process.stdout.write(HELP);
|
|
205
210
|
}
|
|
@@ -241,6 +246,11 @@ async function main(argv) {
|
|
|
241
246
|
process.exit(runWorktree(rest));
|
|
242
247
|
break;
|
|
243
248
|
}
|
|
249
|
+
case 'context': {
|
|
250
|
+
const { runContext } = await import('../../../src/context.mjs');
|
|
251
|
+
process.exit(runContext(rest));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
244
254
|
case 'sync': {
|
|
245
255
|
const { runSync } = await import('../../../src/sync.mjs');
|
|
246
256
|
process.exit(await runSync(rest));
|
|
@@ -14,6 +14,17 @@ export const SHARED_SECTIONS = Object.freeze([
|
|
|
14
14
|
'Último Handoff',
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
+
const SHARED_ADMISSION_SECTIONS = Object.freeze([
|
|
18
|
+
'Bloqueios',
|
|
19
|
+
'Objetivo Atual',
|
|
20
|
+
'Restrições Ativas',
|
|
21
|
+
'Decisões em Vigor',
|
|
22
|
+
'Próximas Ações',
|
|
23
|
+
'Riscos Conhecidos',
|
|
24
|
+
'Último Handoff',
|
|
25
|
+
'Estado Entregue',
|
|
26
|
+
]);
|
|
27
|
+
|
|
17
28
|
const AUTHORITIES = new Set(['verified', 'reported', 'candidate']);
|
|
18
29
|
const OPERATIONS = new Set(['assert', 'replace', 'add', 'remove']);
|
|
19
30
|
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
@@ -197,29 +208,63 @@ function defaultInstant(events) {
|
|
|
197
208
|
return instants.at(-1) || new Date(0).toISOString();
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
function authorityPriority(authority) {
|
|
212
|
+
if (authority === 'verified') return 0;
|
|
213
|
+
if (authority === 'reported') return 1;
|
|
214
|
+
return 2;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function compareProjectionEvents(left, right) {
|
|
218
|
+
return authorityPriority(left?.authority) - authorityPriority(right?.authority)
|
|
219
|
+
|| String(right?.observed_at || '').localeCompare(String(left?.observed_at || ''))
|
|
220
|
+
|| String(left?.projection_key || left?.memory_key || '').localeCompare(
|
|
221
|
+
String(right?.projection_key || right?.memory_key || ''),
|
|
222
|
+
)
|
|
223
|
+
|| String(left?.event_id || '').localeCompare(String(right?.event_id || ''));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function admissionOrder(events) {
|
|
227
|
+
const grouped = new Map(SHARED_ADMISSION_SECTIONS.map((section) => [section, []]));
|
|
228
|
+
for (const event of events) grouped.get(sectionFor(event?.memory_key)).push(event);
|
|
229
|
+
for (const bucket of grouped.values()) bucket.sort(compareProjectionEvents);
|
|
230
|
+
|
|
231
|
+
const ordered = [];
|
|
232
|
+
for (let round = 0; ; round += 1) {
|
|
233
|
+
let admitted = false;
|
|
234
|
+
for (const section of SHARED_ADMISSION_SECTIONS) {
|
|
235
|
+
const event = grouped.get(section)[round];
|
|
236
|
+
if (!event) continue;
|
|
237
|
+
ordered.push(event);
|
|
238
|
+
admitted = true;
|
|
239
|
+
}
|
|
240
|
+
if (!admitted) return ordered;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderSharedMemoryContent({
|
|
245
|
+
revision,
|
|
246
|
+
eventCursor,
|
|
247
|
+
allEvents,
|
|
248
|
+
projectedEvents,
|
|
205
249
|
stateHash,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
const safeEvents = Array.isArray(events) ? events : [];
|
|
210
|
-
const updated = updatedAt || defaultInstant(safeEvents);
|
|
211
|
-
const review = reviewAfter || new Date(Date.parse(updated) + (7 * 24 * 60 * 60 * 1000)).toISOString();
|
|
250
|
+
updated,
|
|
251
|
+
review,
|
|
252
|
+
}) {
|
|
212
253
|
const grouped = new Map(SHARED_SECTIONS.map((section) => [section, []]));
|
|
213
|
-
for (const event of
|
|
254
|
+
for (const event of projectedEvents) grouped.get(sectionFor(event.memory_key)).push(eventLine(event));
|
|
255
|
+
const omittedEvents = allEvents.length - projectedEvents.length;
|
|
214
256
|
|
|
215
257
|
const lines = [
|
|
216
258
|
'---',
|
|
217
259
|
'schema_version: 2',
|
|
218
260
|
`revision: ${Number.isInteger(revision) ? revision : 0}`,
|
|
219
261
|
`event_cursor: ${sanitizeMemoryText(eventCursor || 'none')}`,
|
|
220
|
-
`state_hash: ${sanitizeMemoryText(stateHash || hashProjection(
|
|
262
|
+
`state_hash: ${sanitizeMemoryText(stateHash || hashProjection(allEvents))}`,
|
|
221
263
|
`updated_at: ${sanitizeMemoryText(updated)}`,
|
|
222
264
|
`review_after: ${sanitizeMemoryText(review)}`,
|
|
265
|
+
`projection_mode: ${omittedEvents ? 'bounded' : 'complete'}`,
|
|
266
|
+
`projected_events: ${projectedEvents.length}`,
|
|
267
|
+
`omitted_events: ${omittedEvents}`,
|
|
223
268
|
'---',
|
|
224
269
|
'',
|
|
225
270
|
'# SHARED_MEMORY — projeção operacional gerada',
|
|
@@ -231,6 +276,45 @@ export function renderSharedMemory({
|
|
|
231
276
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
232
277
|
}
|
|
233
278
|
|
|
279
|
+
function actualLineCount(text) {
|
|
280
|
+
const lines = text.split('\n');
|
|
281
|
+
return text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sharedFits(content) {
|
|
285
|
+
return actualLineCount(content) <= SHARED_LIMITS.lines
|
|
286
|
+
&& Buffer.byteLength(content, 'utf8') <= SHARED_LIMITS.bytes;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Render the generated operational projection. Inputs are sanitized a second time. */
|
|
290
|
+
export function renderSharedMemory({
|
|
291
|
+
revision = 0,
|
|
292
|
+
eventCursor = 'none',
|
|
293
|
+
events = [],
|
|
294
|
+
stateHash,
|
|
295
|
+
updatedAt,
|
|
296
|
+
reviewAfter,
|
|
297
|
+
} = {}) {
|
|
298
|
+
const safeEvents = Array.isArray(events) ? events : [];
|
|
299
|
+
const updated = updatedAt || defaultInstant(safeEvents);
|
|
300
|
+
const review = reviewAfter || new Date(Date.parse(updated) + (7 * 24 * 60 * 60 * 1000)).toISOString();
|
|
301
|
+
const render = (projectedEvents) => renderSharedMemoryContent({
|
|
302
|
+
revision,
|
|
303
|
+
eventCursor,
|
|
304
|
+
allEvents: safeEvents,
|
|
305
|
+
projectedEvents,
|
|
306
|
+
stateHash,
|
|
307
|
+
updated,
|
|
308
|
+
review,
|
|
309
|
+
});
|
|
310
|
+
const projectedEvents = [];
|
|
311
|
+
for (const event of admissionOrder(safeEvents)) {
|
|
312
|
+
const trial = [...projectedEvents, event];
|
|
313
|
+
if (sharedFits(render(trial))) projectedEvents.push(event);
|
|
314
|
+
}
|
|
315
|
+
return render(projectedEvents);
|
|
316
|
+
}
|
|
317
|
+
|
|
234
318
|
function parseScalar(value) {
|
|
235
319
|
if (/^-?\d+$/.test(value)) return Number(value);
|
|
236
320
|
return value;
|
|
@@ -278,11 +362,6 @@ export function parseSharedMemory(content) {
|
|
|
278
362
|
return { ok: errors.length === 0, errors, metadata, sections };
|
|
279
363
|
}
|
|
280
364
|
|
|
281
|
-
function actualLineCount(text) {
|
|
282
|
-
const lines = text.split('\n');
|
|
283
|
-
return text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
365
|
export function validateSharedMemory(content, { eventIds } = {}) {
|
|
287
366
|
const text = String(content ?? '').replace(/\r\n/g, '\n');
|
|
288
367
|
const parsed = parseSharedMemory(text);
|
|
@@ -306,6 +385,31 @@ export function validateSharedMemory(content, { eventIds } = {}) {
|
|
|
306
385
|
for (const key of ['event_cursor', 'state_hash', 'updated_at', 'review_after']) {
|
|
307
386
|
if (typeof metadata[key] !== 'string' || !metadata[key]) errors.push(`${key} é obrigatório.`);
|
|
308
387
|
}
|
|
388
|
+
const projectionFields = ['projection_mode', 'projected_events', 'omitted_events'];
|
|
389
|
+
const projectionFieldCount = projectionFields.filter((key) => metadata[key] !== undefined).length;
|
|
390
|
+
if (projectionFieldCount && projectionFieldCount !== projectionFields.length) {
|
|
391
|
+
errors.push('Metadados bounded incompletos: projection_mode, projected_events e omitted_events são inseparáveis.');
|
|
392
|
+
} else if (projectionFieldCount === projectionFields.length) {
|
|
393
|
+
const projectedEventLines = [...parsed.sections.values()].flat()
|
|
394
|
+
.filter((line) => /^\s*-\s+\[[^\]]+\]/.test(line)).length;
|
|
395
|
+
if (!['complete', 'bounded'].includes(metadata.projection_mode)) {
|
|
396
|
+
errors.push('projection_mode deve ser complete ou bounded.');
|
|
397
|
+
}
|
|
398
|
+
if (!Number.isInteger(metadata.projected_events) || metadata.projected_events < 0) {
|
|
399
|
+
errors.push('projected_events deve ser inteiro não negativo.');
|
|
400
|
+
} else if (metadata.projected_events !== projectedEventLines) {
|
|
401
|
+
errors.push(`projected_events declara ${metadata.projected_events}, mas SHARED contém ${projectedEventLines} evento(s).`);
|
|
402
|
+
}
|
|
403
|
+
if (!Number.isInteger(metadata.omitted_events) || metadata.omitted_events < 0) {
|
|
404
|
+
errors.push('omitted_events deve ser inteiro não negativo.');
|
|
405
|
+
}
|
|
406
|
+
if (metadata.projection_mode === 'complete' && metadata.omitted_events !== 0) {
|
|
407
|
+
errors.push('projection_mode complete exige omitted_events igual a 0.');
|
|
408
|
+
}
|
|
409
|
+
if (metadata.projection_mode === 'bounded' && !(metadata.omitted_events > 0)) {
|
|
410
|
+
errors.push('projection_mode bounded exige omitted_events maior que 0.');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
309
413
|
for (const key of ['updated_at', 'review_after']) {
|
|
310
414
|
if (typeof metadata[key] === 'string'
|
|
311
415
|
&& (!ISO_INSTANT.test(metadata[key]) || Number.isNaN(Date.parse(metadata[key])))) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import {
|
|
13
|
+
parseSharedMemory,
|
|
13
14
|
renderSharedMemory,
|
|
14
15
|
sanitizeMemoryText,
|
|
15
16
|
validateMemoryEvent,
|
|
@@ -1007,6 +1008,7 @@ export function prepareMemoryProjection(vaultBase, allEvents) {
|
|
|
1007
1008
|
stateHash: reduced.stateHash,
|
|
1008
1009
|
updatedAt,
|
|
1009
1010
|
});
|
|
1011
|
+
const sharedMetadata = parseSharedMemory(shared).metadata;
|
|
1010
1012
|
const candidates = reduced.candidates.map((item) => canonicalMemoryJson(item)).join('\n')
|
|
1011
1013
|
+ (reduced.candidates.length ? '\n' : '');
|
|
1012
1014
|
return {
|
|
@@ -1018,6 +1020,8 @@ export function prepareMemoryProjection(vaultBase, allEvents) {
|
|
|
1018
1020
|
stateHash: reduced.stateHash,
|
|
1019
1021
|
checkpoint: reduced.checkpoint,
|
|
1020
1022
|
candidates: reduced.candidates.length,
|
|
1023
|
+
projectedEvents: sharedMetadata.projected_events ?? reduced.activeEvents.length,
|
|
1024
|
+
omittedEvents: sharedMetadata.omitted_events ?? 0,
|
|
1021
1025
|
};
|
|
1022
1026
|
}
|
|
1023
1027
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, join, relative } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseSharedMemory, renderSharedMemory, sanitizeMemoryText, validateMemoryEvent, validateSharedMemory,
|
|
5
|
+
} from './memory-schema.mjs';
|
|
4
6
|
import { deriveMemoryProjection } from './memory-store.mjs';
|
|
5
7
|
import { assertVaultPathSafe } from './vault-path-safety.mjs';
|
|
6
8
|
import { validateCore } from './validate-core.mjs';
|
|
@@ -161,6 +163,10 @@ function sharedEventIds(content) {
|
|
|
161
163
|
return ids;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function sameStringSet(left, right) {
|
|
167
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
168
|
+
}
|
|
169
|
+
|
|
164
170
|
function walkDecisionFiles(root, vaultBase, output = []) {
|
|
165
171
|
let entries;
|
|
166
172
|
try { entries = readdirSync(root, { withFileTypes: true }); } catch { return output; }
|
|
@@ -255,13 +261,41 @@ function semanticMemoryHealth(vaultBase, { ledger, shared, candidates }) {
|
|
|
255
261
|
const errors = [];
|
|
256
262
|
const warnings = [];
|
|
257
263
|
const codes = [];
|
|
264
|
+
const metadata = shared.metadata || {};
|
|
265
|
+
const boundedDeclared = metadata.projection_mode === 'bounded';
|
|
266
|
+
let boundedProjection = false;
|
|
267
|
+
if (boundedDeclared) {
|
|
268
|
+
const expectedContent = renderSharedMemory({
|
|
269
|
+
revision: metadata.revision,
|
|
270
|
+
eventCursor: metadata.event_cursor,
|
|
271
|
+
stateHash: metadata.state_hash,
|
|
272
|
+
updatedAt: metadata.updated_at,
|
|
273
|
+
reviewAfter: metadata.review_after,
|
|
274
|
+
events: replay.activeEvents,
|
|
275
|
+
});
|
|
276
|
+
const expectedIds = sharedEventIds(expectedContent);
|
|
277
|
+
const expectedMetadata = parseSharedMemory(expectedContent).metadata;
|
|
278
|
+
boundedProjection = sameStringSet(projectedIds, expectedIds)
|
|
279
|
+
&& metadata.projected_events === expectedMetadata.projected_events
|
|
280
|
+
&& metadata.omitted_events === expectedMetadata.omitted_events;
|
|
281
|
+
if (!boundedProjection) {
|
|
282
|
+
codes.push('MEMORY_SEMANTIC_COVERAGE_MISSING');
|
|
283
|
+
errors.push('[MEMORY_SEMANTIC_COVERAGE_MISSING] SHARED declara bounded, mas IDs ou contagens não reproduzem a seleção do ledger.');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
258
286
|
|
|
259
287
|
if (missingKeys.length && placeholderOnly) {
|
|
260
288
|
codes.push('MEMORY_SEMANTIC_PLACEHOLDER_ONLY');
|
|
261
289
|
errors.push(`[MEMORY_SEMANTIC_PLACEHOLDER_ONLY] SHARED contém somente placeholders para ${activeKeys.length} chave(s) ativa(s); candidates=${candidateCount}.`);
|
|
262
290
|
} else if (missingKeys.length) {
|
|
263
|
-
|
|
264
|
-
|
|
291
|
+
if (boundedProjection) {
|
|
292
|
+
warnings.push(`[MEMORY_SEMANTIC_BOUNDED_PROJECTION] SHARED omite deterministicamente ${metadata.omitted_events} evento(s) do ledger dentro do budget.`);
|
|
293
|
+
} else if (!boundedDeclared) {
|
|
294
|
+
codes.push('MEMORY_SEMANTIC_COVERAGE_MISSING');
|
|
295
|
+
errors.push(`[MEMORY_SEMANTIC_COVERAGE_MISSING] SHARED não cobre ${missingKeys.length} chave(s) ativa(s): ${missingKeys.join(', ')}.`);
|
|
296
|
+
}
|
|
297
|
+
} else if (boundedProjection) {
|
|
298
|
+
warnings.push(`[MEMORY_SEMANTIC_BOUNDED_PROJECTION] SHARED omite deterministicamente ${metadata.omitted_events} evento(s) do ledger dentro do budget.`);
|
|
265
299
|
}
|
|
266
300
|
if (unresolvedLinks.length) {
|
|
267
301
|
codes.push('MEMORY_SEMANTIC_DECISION_LINK_UNRESOLVED');
|
|
@@ -280,6 +314,9 @@ function semanticMemoryHealth(vaultBase, { ledger, shared, candidates }) {
|
|
|
280
314
|
} else if (codes.length) {
|
|
281
315
|
status = 'degraded';
|
|
282
316
|
code = codes[0];
|
|
317
|
+
} else if (boundedProjection) {
|
|
318
|
+
status = 'bounded';
|
|
319
|
+
code = 'MEMORY_SEMANTIC_BOUNDED_PROJECTION';
|
|
283
320
|
}
|
|
284
321
|
|
|
285
322
|
return {
|
package/src/context.mjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
mutateSessionRegistry,
|
|
5
|
+
readSessionRegistry,
|
|
6
|
+
} from '../hooks/obsidian-common.mjs';
|
|
7
|
+
import {
|
|
8
|
+
captureProjectScope,
|
|
9
|
+
compareProjectScopes,
|
|
10
|
+
concurrentScopeConflicts,
|
|
11
|
+
scopeForRegistry,
|
|
12
|
+
} from '../hooks/project-scope.mjs';
|
|
13
|
+
|
|
14
|
+
export const CONTEXT_HELP = `wendkeep context <subcommand>
|
|
15
|
+
|
|
16
|
+
switch <branch> [--create] [--session <id>] [--project <path>] [--vault <path>] [--json]
|
|
17
|
+
|
|
18
|
+
Switches Git branch and the causal session scope together inside the same worktree.
|
|
19
|
+
Without --session, exactly one active session must match the current scope.
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const VALUE_OPTIONS = new Set(['--project', '--vault', '--session']);
|
|
23
|
+
const FLAG_OPTIONS = new Set(['--create', '--json']);
|
|
24
|
+
|
|
25
|
+
function contextError(code, message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.code = code;
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function optionValue(argv, name) {
|
|
32
|
+
const index = argv.indexOf(name);
|
|
33
|
+
if (index >= 0) return argv[index + 1] || '';
|
|
34
|
+
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function positionals(argv) {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
40
|
+
const value = argv[index];
|
|
41
|
+
if (VALUE_OPTIONS.has(value)) { index += 1; continue; }
|
|
42
|
+
if ([...VALUE_OPTIONS].some((name) => value.startsWith(`${name}=`))) continue;
|
|
43
|
+
if (FLAG_OPTIONS.has(value)) continue;
|
|
44
|
+
if (value.startsWith('--')) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção desconhecida: ${value}`);
|
|
45
|
+
result.push(value);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateArgv(argv) {
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
53
|
+
const value = argv[index];
|
|
54
|
+
if (FLAG_OPTIONS.has(value)) {
|
|
55
|
+
if (seen.has(value)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${value}`);
|
|
56
|
+
seen.add(value);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (VALUE_OPTIONS.has(value)) {
|
|
60
|
+
if (seen.has(value)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${value}`);
|
|
61
|
+
seen.add(value);
|
|
62
|
+
const next = argv[index + 1];
|
|
63
|
+
if (!next || next.startsWith('--')) throw contextError('WENDKEEP_CONTEXT_ARGS', `${value} requer um valor`);
|
|
64
|
+
index += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (value.startsWith('--')) {
|
|
68
|
+
const name = value.split('=', 1)[0];
|
|
69
|
+
if (!VALUE_OPTIONS.has(name)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção desconhecida: ${name}`);
|
|
70
|
+
if (seen.has(name)) throw contextError('WENDKEEP_CONTEXT_ARGS', `opção duplicada: ${name}`);
|
|
71
|
+
seen.add(name);
|
|
72
|
+
if (!value.slice(name.length + 1)) throw contextError('WENDKEEP_CONTEXT_ARGS', `${name} requer um valor`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function vaultOf(argv) {
|
|
78
|
+
const raw = optionValue(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
79
|
+
if (!raw) throw contextError('WENDKEEP_CONTEXT_VAULT', 'binding de Vault ausente; use --vault <path>');
|
|
80
|
+
return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function projectOf(argv) {
|
|
84
|
+
const raw = optionValue(argv, '--project') || process.cwd();
|
|
85
|
+
return isAbsolute(raw) ? raw : resolve(process.cwd(), raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function git(projectRoot, args, spawn = spawnSync) {
|
|
89
|
+
const result = spawn('git', args, { cwd: projectRoot, encoding: 'utf8', windowsHide: true });
|
|
90
|
+
if (result.error || result.status !== 0) {
|
|
91
|
+
const detail = String(result.stderr || result.error?.message || 'falhou').trim();
|
|
92
|
+
throw contextError('WENDKEEP_CONTEXT_GIT', `git ${args.join(' ')}: ${detail}`);
|
|
93
|
+
}
|
|
94
|
+
return String(result.stdout || '').trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function actualScope(projectRoot, expected, sessionId, spawn) {
|
|
98
|
+
return captureProjectScope({
|
|
99
|
+
input: { cwd: projectRoot },
|
|
100
|
+
projectRoot: expected?.projectRoot || projectRoot,
|
|
101
|
+
projectId: expected?.projectId || '',
|
|
102
|
+
provider: expected?.provider || '',
|
|
103
|
+
sessionId,
|
|
104
|
+
targetCwd: projectRoot,
|
|
105
|
+
spawn,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function matchingSessionIds(registry, projectRoot, spawn) {
|
|
110
|
+
const matches = [];
|
|
111
|
+
for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
|
|
112
|
+
if (entry?.status !== 'active' || entry.project_scope_conflict === true || !entry?.project_scope) continue;
|
|
113
|
+
const actual = actualScope(projectRoot, entry.project_scope, sessionId, spawn);
|
|
114
|
+
if (compareProjectScopes(entry.project_scope, actual).ok) matches.push(sessionId);
|
|
115
|
+
}
|
|
116
|
+
return matches;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function contextRevision(entry) {
|
|
120
|
+
return Number.isSafeInteger(entry?.context_revision) && entry.context_revision >= 0
|
|
121
|
+
? entry.context_revision : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveSessionId(vaultBase, projectRoot, requested, spawn) {
|
|
125
|
+
const registry = readSessionRegistry(vaultBase);
|
|
126
|
+
if (requested) {
|
|
127
|
+
const entry = registry.sessions?.[requested];
|
|
128
|
+
if (!entry || entry.status !== 'active') {
|
|
129
|
+
throw contextError('WENDKEEP_CONTEXT_SESSION', `sessão ativa não encontrada: ${requested}`);
|
|
130
|
+
}
|
|
131
|
+
return { sessionId: requested, revision: contextRevision(entry) };
|
|
132
|
+
}
|
|
133
|
+
const matches = matchingSessionIds(registry, projectRoot, spawn);
|
|
134
|
+
if (matches.length !== 1) {
|
|
135
|
+
throw contextError(
|
|
136
|
+
'WENDKEEP_CONTEXT_AMBIGUOUS',
|
|
137
|
+
`${matches.length} sessões ativas correspondem à scope atual; informe --session <id>.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
sessionId: matches[0],
|
|
142
|
+
revision: contextRevision(registry.sessions[matches[0]]),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rollbackGit(projectRoot, previous, createdBranch, spawn) {
|
|
147
|
+
const args = previous.branch.startsWith('detached:')
|
|
148
|
+
? ['switch', '--detach', previous.head]
|
|
149
|
+
: ['switch', previous.branch];
|
|
150
|
+
git(projectRoot, args, spawn);
|
|
151
|
+
if (createdBranch) git(projectRoot, ['branch', '-D', createdBranch], spawn);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function switchSessionContext({
|
|
155
|
+
vaultBase,
|
|
156
|
+
projectRoot = process.cwd(),
|
|
157
|
+
branch,
|
|
158
|
+
create = false,
|
|
159
|
+
sessionId = '',
|
|
160
|
+
spawn = spawnSync,
|
|
161
|
+
mutateRegistry = mutateSessionRegistry,
|
|
162
|
+
now = () => new Date(),
|
|
163
|
+
} = {}) {
|
|
164
|
+
const target = String(branch || '').trim();
|
|
165
|
+
if (!target) throw contextError('WENDKEEP_CONTEXT_ARGS', 'switch requer <branch>');
|
|
166
|
+
git(projectRoot, ['check-ref-format', '--branch', target], spawn);
|
|
167
|
+
const selected = resolveSessionId(vaultBase, projectRoot, sessionId, spawn);
|
|
168
|
+
const selectedSessionId = selected.sessionId;
|
|
169
|
+
let switched = false;
|
|
170
|
+
let previous = null;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return mutateRegistry(vaultBase, (registry) => {
|
|
174
|
+
const entry = registry.sessions?.[selectedSessionId];
|
|
175
|
+
if (!entry || entry.status !== 'active') {
|
|
176
|
+
throw contextError('WENDKEEP_CONTEXT_SESSION', `sessão ativa não encontrada: ${selectedSessionId}`);
|
|
177
|
+
}
|
|
178
|
+
if (entry.project_scope_conflict === true || !entry.project_scope) {
|
|
179
|
+
throw contextError('WENDKEEP_CONTEXT_SCOPE_CONFLICT', 'a sessão possui scope ausente ou conflitante');
|
|
180
|
+
}
|
|
181
|
+
const currentRevision = contextRevision(entry);
|
|
182
|
+
if (currentRevision !== selected.revision) {
|
|
183
|
+
throw contextError(
|
|
184
|
+
'WENDKEEP_CONTEXT_CAS_MISMATCH',
|
|
185
|
+
`context_revision mudou de ${selected.revision} para ${currentRevision}; repita com estado fresco`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const expected = entry.project_scope;
|
|
189
|
+
const actual = actualScope(projectRoot, expected, selectedSessionId, spawn);
|
|
190
|
+
const comparison = compareProjectScopes(expected, actual);
|
|
191
|
+
if (!comparison.ok) {
|
|
192
|
+
throw contextError(
|
|
193
|
+
'WENDKEEP_CONTEXT_SCOPE_MISMATCH',
|
|
194
|
+
`scope atual diverge da reserva (${comparison.mismatches.join(', ')})`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (actual.branch === target && !create) {
|
|
198
|
+
return {
|
|
199
|
+
status: 'unchanged', session_id: selectedSessionId, branch: target,
|
|
200
|
+
head: actual.head, revision: currentRevision,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
previous = { branch: actual.branch, head: actual.head };
|
|
205
|
+
git(projectRoot, create ? ['switch', '-c', target] : ['switch', target], spawn);
|
|
206
|
+
switched = true;
|
|
207
|
+
const next = actualScope(projectRoot, expected, selectedSessionId, spawn);
|
|
208
|
+
const identity = compareProjectScopes({ ...expected, branch: next.branch }, next);
|
|
209
|
+
if (!identity.ok || next.branch !== target || next.worktree !== actual.worktree) {
|
|
210
|
+
const mismatches = [...identity.mismatches, ...(next.branch === target ? [] : ['scope.branch'])];
|
|
211
|
+
throw contextError(
|
|
212
|
+
'WENDKEEP_CONTEXT_IDENTITY_CHANGED',
|
|
213
|
+
`a transição saiu da identidade reservada (${[...new Set(mismatches)].join(', ')})`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const conflicts = concurrentScopeConflicts(
|
|
217
|
+
next,
|
|
218
|
+
Object.entries(registry.sessions || {}).filter(([, candidate]) => candidate?.status === 'active'),
|
|
219
|
+
selectedSessionId,
|
|
220
|
+
);
|
|
221
|
+
if (conflicts.length) {
|
|
222
|
+
throw contextError(
|
|
223
|
+
'WENDKEEP_CONTEXT_CONFLICT',
|
|
224
|
+
`o destino conflita com ${conflicts.length} contexto(s) ativo(s): ${conflicts.map((item) => item.sessionId).join(', ')}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const revision = currentRevision + 1;
|
|
229
|
+
const at = now().toISOString();
|
|
230
|
+
const transition = {
|
|
231
|
+
revision,
|
|
232
|
+
operation: create ? 'create' : 'switch',
|
|
233
|
+
from: { branch: actual.branch, head: actual.head },
|
|
234
|
+
to: { branch: next.branch, head: next.head },
|
|
235
|
+
worktree: next.worktree,
|
|
236
|
+
at,
|
|
237
|
+
};
|
|
238
|
+
const {
|
|
239
|
+
project_scope_conflict: _conflict,
|
|
240
|
+
project_scope_conflict_fields: _conflictFields,
|
|
241
|
+
project_scope_observed: _observed,
|
|
242
|
+
...preserved
|
|
243
|
+
} = entry;
|
|
244
|
+
registry.sessions[selectedSessionId] = {
|
|
245
|
+
...preserved,
|
|
246
|
+
project_scope: scopeForRegistry(next, { authorizedActions: expected.authorizedActions }),
|
|
247
|
+
context_revision: revision,
|
|
248
|
+
context_transitions: [...(Array.isArray(entry.context_transitions) ? entry.context_transitions : []), transition],
|
|
249
|
+
last_seen: at,
|
|
250
|
+
updated_at: at,
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
status: 'switched', session_id: selectedSessionId, branch: next.branch,
|
|
254
|
+
head: next.head, revision, transition,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (switched && previous) {
|
|
259
|
+
try {
|
|
260
|
+
rollbackGit(projectRoot, previous, create ? target : '', spawn);
|
|
261
|
+
} catch (rollbackError) {
|
|
262
|
+
throw contextError(
|
|
263
|
+
'WENDKEEP_CONTEXT_ROLLBACK_FAILED',
|
|
264
|
+
`${error.code || 'WENDKEEP_CONTEXT_FAILED'}: ${error.message}; rollback: ${rollbackError.message}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function output(result, json) {
|
|
273
|
+
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
274
|
+
else process.stdout.write(`context ${result.status}: ${result.branch} (session ${result.session_id}; revision ${result.revision})\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function runContext(argv = []) {
|
|
278
|
+
try {
|
|
279
|
+
validateArgv(argv);
|
|
280
|
+
const [sub, branch, ...extra] = positionals(argv);
|
|
281
|
+
if (sub !== 'switch' || !branch || extra.length) {
|
|
282
|
+
throw contextError('WENDKEEP_CONTEXT_ARGS', 'use: wendkeep context switch <branch> [--create] [--session <id>]');
|
|
283
|
+
}
|
|
284
|
+
const result = switchSessionContext({
|
|
285
|
+
vaultBase: vaultOf(argv),
|
|
286
|
+
projectRoot: projectOf(argv),
|
|
287
|
+
branch,
|
|
288
|
+
create: argv.includes('--create'),
|
|
289
|
+
sessionId: optionValue(argv, '--session'),
|
|
290
|
+
});
|
|
291
|
+
output(result, argv.includes('--json'));
|
|
292
|
+
return 0;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
process.stderr.write(`wendkeep context: ${error.code || 'WENDKEEP_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
295
|
+
return 2;
|
|
296
|
+
}
|
|
297
|
+
}
|