wendkeep 0.76.3 → 0.76.5
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 +24 -0
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/docs/en/commands/changes-and-verification.md +12 -5
- package/docs/en/commands/context.md +16 -0
- package/docs/en/commands/operating-profiles.md +13 -4
- package/docs/pt-BR/commands/changes-and-verification.md +12 -5
- package/docs/pt-BR/commands/context.md +16 -0
- package/docs/pt-BR/commands/operating-profiles.md +13 -4
- package/hooks/active-context-store.mjs +271 -0
- package/hooks/change-context.mjs +11 -2
- package/hooks/change-core.mjs +35 -17
- package/hooks/change-warn.mjs +12 -3
- package/hooks/obsidian-common.mjs +5 -2
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +3 -2
- package/src/active-context-runtime.mjs +131 -0
- package/src/change.mjs +39 -10
- package/src/delivery.mjs +134 -22
- package/src/spec.mjs +15 -2
- package/src/verify.mjs +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.76.5] — 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Delivery causal.** `delivery_id` passa a pertencer ao `active_contexts` da worktree/work session;
|
|
12
|
+
start, status, finish e abandon isolam deliveries concorrentes e rejeitam IDs de outro contexto.
|
|
13
|
+
- **Hooks escopados.** `change-context` e `change-warn` só reconhecem a autorização de delivery do
|
|
14
|
+
chamador causal, sem herdar permissões de sessões irmãs.
|
|
15
|
+
- **Compatibilidade conservadora.** `CURRENT_DELIVERY` vira projeção somente para um contexto
|
|
16
|
+
inequívoco; falha de bind remove o estado recém-criado sem publicar ponteiro parcial.
|
|
17
|
+
|
|
18
|
+
## [0.76.4] — 2026-08-22
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Registry multi-contexto.** `active_contexts` mantém change, branch e HEAD por
|
|
23
|
+
`repository_id` + `worktree_id` + `work_session_id`, com schema, revisão, CAS e writes atômicos;
|
|
24
|
+
duas worktrees ou sessões deixam de sobrescrever o foco operacional umas das outras.
|
|
25
|
+
- **Resolução causal de change.** Change, spec e verify usam a sessão/worktree chamadora; ausência
|
|
26
|
+
de sessão só aceita um contexto inequívoco e qualquer ambiguidade falha fechado.
|
|
27
|
+
- **Compatibilidade conservadora.** `CURRENT_CHANGE.md` é somente projeção quando há um único
|
|
28
|
+
contexto ativo, e a migração legada não inventa worktree ou sessão quando a identidade não pode
|
|
29
|
+
ser provada.
|
|
30
|
+
|
|
7
31
|
## [0.76.3] — 2026-08-21
|
|
8
32
|
|
|
9
33
|
### 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. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
118
|
+
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
119
119
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
120
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
121
|
- **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
|
|
@@ -239,7 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
|
|
|
239
239
|
|---|---|---|
|
|
240
240
|
| **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
|
|
241
241
|
| **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
|
|
242
|
-
| **Active context** | `
|
|
242
|
+
| **Active context** | `active_contexts` by `repository_id`/`worktree_id`/`work_session_id`, causal transition, quarantine, and explicit recovery | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
|
|
243
243
|
| **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
|
|
244
244
|
| **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
|
|
245
245
|
| **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
|
|
@@ -338,10 +338,15 @@ Work kind, profile, contract impact, and operational risk are independent dimens
|
|
|
338
338
|
spec, or ADR. If delivery requires a code/config edit, it pauses and work returns to
|
|
339
339
|
`implementation`:
|
|
340
340
|
|
|
341
|
+
In multi-context Vaults, `active_contexts[].delivery_id` is authoritative and `--session <id>`
|
|
342
|
+
selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection when there is one
|
|
343
|
+
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
|
+
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
345
|
+
|
|
341
346
|
```bash
|
|
342
|
-
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha>
|
|
343
|
-
npx wendkeep delivery status release-0-74-0
|
|
344
|
-
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url>
|
|
347
|
+
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha> --session <id>
|
|
348
|
+
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
349
|
+
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url> --session <id>
|
|
345
350
|
```
|
|
346
351
|
|
|
347
352
|
If the harness does not record a lease, a small fix remains under the configured profile —
|
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ npx wendkeep init
|
|
|
115
115
|
|
|
116
116
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
117
117
|
2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
|
|
118
|
-
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
118
|
+
3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. A raw `git checkout/switch` branch transition is denied before it can strand the session; use `wendkeep context switch <branch> [--create]`, which moves Git and the causal scope together in the same worktree with an audited revision and rollback. If a divergence is already quarantined, `context status --session <id>` inventories sanitized `reserved`/`observed` candidates; `context recover --session <id> --select <reserved|observed> --revision <n> --reason <text>` requires an explicit choice, CAS, and current-checkout proof, failing closed before clearing the conflict if revalidation changes. The change lifecycle uses `active_contexts`, identified by `repository_id` + `worktree_id` + `work_session_id`; two matching sessions fail with ambiguity instead of selecting silently, `CURRENT_CHANGE.md` is only a derived projection for one unambiguous context, and migration never invents a worktree or session identity. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
|
|
119
119
|
4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
|
|
120
120
|
5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
|
|
121
121
|
- **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
|
|
@@ -239,7 +239,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
|
|
|
239
239
|
|---|---|---|
|
|
240
240
|
| **Installation and updates** | `init`, `sync`, companions, and the first project↔vault binding | [Installation and first use](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/getting-started.md) |
|
|
241
241
|
| **Managed worktrees** | `worktree create/list/status/open`, Vault binding, and recovery | [Managed worktrees](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/worktrees.md) |
|
|
242
|
-
| **Active context** | `
|
|
242
|
+
| **Active context** | `active_contexts` by `repository_id`/`worktree_id`/`work_session_id`, causal transition, quarantine, and explicit recovery | [Active context](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/context.md) |
|
|
243
243
|
| **Operating profiles** | `profile`, `flow`, always-on Keep Core, and Wend Runtime governance | [Operating profiles](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/operating-profiles.md) |
|
|
244
244
|
| **Changes and verification** | `change`, specs, sensors, TDD, evidence, and archive | [Changes and verification](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/changes-and-verification.md) |
|
|
245
245
|
| **Shared memory** | CORE, SHARED, status, validation, repair, and curation | [Memory](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory.md) |
|
|
@@ -338,10 +338,15 @@ Work kind, profile, contract impact, and operational risk are independent dimens
|
|
|
338
338
|
spec, or ADR. If delivery requires a code/config edit, it pauses and work returns to
|
|
339
339
|
`implementation`:
|
|
340
340
|
|
|
341
|
+
In multi-context Vaults, `active_contexts[].delivery_id` is authoritative and `--session <id>`
|
|
342
|
+
selects the caller explicitly. `CURRENT_DELIVERY` is only a derived projection when there is one
|
|
343
|
+
single, unambiguous context; hooks consult only the causal delivery, and an ID from another context
|
|
344
|
+
fails with `WENDKEEP_DELIVERY_CONTEXT_MISMATCH`.
|
|
345
|
+
|
|
341
346
|
```bash
|
|
342
|
-
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha>
|
|
343
|
-
npx wendkeep delivery status release-0-74-0
|
|
344
|
-
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url>
|
|
347
|
+
npx wendkeep delivery start release-0-74-0 --allow git:merge --allow git:push --allow publish --source-change <slug> --source-commit <sha> --session <id>
|
|
348
|
+
npx wendkeep delivery status release-0-74-0 --session <id>
|
|
349
|
+
npx wendkeep delivery finish release-0-74-0 --target main --ci-url <url> --version 0.74.0 --npm-integrity <sha512> --release-url <url> --session <id>
|
|
345
350
|
```
|
|
346
351
|
|
|
347
352
|
If the harness does not record a lease, a small fix remains under the configured profile —
|
|
@@ -25,12 +25,12 @@ Initialize the project, keep the vault healthy, and provide a valid `wendkeep.se
|
|
|
25
25
|
## Syntax
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx wendkeep change new <slug> [--simple|--guide]
|
|
29
|
-
npx wendkeep change status [slug]
|
|
30
|
-
npx wendkeep spec effective --change <slug>
|
|
28
|
+
npx wendkeep change new <slug> [--simple|--guide] [--session <id>]
|
|
29
|
+
npx wendkeep change status [slug] [--session <id>]
|
|
30
|
+
npx wendkeep spec effective [--change <slug>] [--session <id>]
|
|
31
31
|
npx wendkeep sensors list
|
|
32
|
-
npx wendkeep verify [--deep] [--change <slug>]
|
|
33
|
-
npx wendkeep change archive <slug>
|
|
32
|
+
npx wendkeep verify [--deep] [--change <slug>] [--session <id>]
|
|
33
|
+
npx wendkeep change archive <slug> [--session <id>]
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Options and exit codes
|
|
@@ -43,6 +43,8 @@ npx wendkeep change archive <slug>
|
|
|
43
43
|
without archiving it.
|
|
44
44
|
- `change continue <archived> <new>` starts follow-up work without inheriting stale proof.
|
|
45
45
|
- `change bind <slug> --session <id>` attaches an existing session.
|
|
46
|
+
- `--session <id>` selects the causal `active_contexts` entry for implicit commands. Without it,
|
|
47
|
+
only one unambiguous active context for the worktree is accepted; ambiguity returns exit `2`.
|
|
46
48
|
- `change relink [--apply]` and `change backlink [--apply]` repair graph links; preview is default.
|
|
47
49
|
- `change abandon <slug>` drops work without an ADR; `archive --force` needs explicit human choice.
|
|
48
50
|
- `wendkeep spec list|show|effective|migrate|rebase` manages living contracts and deltas.
|
|
@@ -54,6 +56,7 @@ npx wendkeep change archive <slug>
|
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
58
|
npx wendkeep change new tenant-login
|
|
59
|
+
npx wendkeep change use tenant-login --session <id>
|
|
57
60
|
npx wendkeep change new internal-adjustment --guide
|
|
58
61
|
npx wendkeep spec effective --change tenant-login
|
|
59
62
|
npx wendkeep change done 1.1 --change tenant-login
|
|
@@ -82,6 +85,10 @@ tool runs, it compares the session, project, Git root, remote, branch, and workt
|
|
|
82
85
|
recorded in `SESSION_REGISTRY.json`. Missing, ambiguous, concurrent, or cross-project targets are
|
|
83
86
|
blocked before the tool.
|
|
84
87
|
|
|
88
|
+
Implicit change focus comes from `active_contexts`, not `CURRENT_CHANGE.md`. Its key combines
|
|
89
|
+
`repository_id`, `worktree_id`, and `work_session_id`; the Markdown pointer remains only a
|
|
90
|
+
compatibility projection when there is one unambiguous context.
|
|
91
|
+
|
|
85
92
|
The [local Observer](observer.md) is a read-only observability projection: the vault and change
|
|
86
93
|
remain local authorities. Observer queries do not complete, archive, repair, or promote state in a
|
|
87
94
|
vault.
|
|
@@ -85,6 +85,22 @@ preserves the change/lease/authorizations, clears only the quarantine, and appen
|
|
|
85
85
|
receipt to `context_recoveries`. Any failure leaves the registry and quarantine byte-identical.
|
|
86
86
|
This is fail-closed: no candidate, receipt, or partial scope is published after a failed check.
|
|
87
87
|
|
|
88
|
+
### Multi-context change registry
|
|
89
|
+
|
|
90
|
+
`SESSION_REGISTRY.json` keeps `active_contexts` with its own schema and revision. Each entry is
|
|
91
|
+
identified by `repository_id` + `worktree_id` + `work_session_id`; branch, HEAD, and `change_slug`
|
|
92
|
+
belong to that entry. Two worktrees can therefore select different changes without overwriting
|
|
93
|
+
each other's operational focus.
|
|
94
|
+
|
|
95
|
+
With an explicit causal session, change, spec, and verify resolve only the matching entry. Without
|
|
96
|
+
a session, only one active entry for the worktree is accepted; two sessions produce ambiguity and
|
|
97
|
+
the operation must fail closed without silently selecting a change.
|
|
98
|
+
|
|
99
|
+
`CURRENT_CHANGE.md` is only a derived projection: it contains a change when there is one single,
|
|
100
|
+
unambiguous active context. With zero or multiple contexts it stays empty. Migration is conservative
|
|
101
|
+
and never invents a worktree or session identity. The legacy pointer becomes a context only
|
|
102
|
+
when one active session, a complete scope, and worktree metadata prove one identity.
|
|
103
|
+
|
|
88
104
|
## Common errors and diagnosis
|
|
89
105
|
|
|
90
106
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: pass `--session <id>`; no candidate is selected silently.
|
|
@@ -57,10 +57,10 @@ npx wendkeep flow status [<id>]
|
|
|
57
57
|
npx wendkeep flow show <id> [--session <id>]
|
|
58
58
|
npx wendkeep flow finish <id> [--session <id>]
|
|
59
59
|
npx wendkeep flow promote <id> [--change-slug <slug>] [--session <id>]
|
|
60
|
-
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
61
|
-
npx wendkeep delivery status [id]
|
|
62
|
-
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>]
|
|
63
|
-
npx wendkeep delivery abandon [id] --reason <text>
|
|
60
|
+
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>] [--session <id>]
|
|
61
|
+
npx wendkeep delivery status [id] [--session <id>]
|
|
62
|
+
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>] [--session <id>]
|
|
63
|
+
npx wendkeep delivery abandon [id] --reason <text> [--session <id>]
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Every FLOW subcommand also accepts `--project <path>`, `--vault <path>`, and `--json`. When
|
|
@@ -178,6 +178,15 @@ ownership to the native LLM harness.
|
|
|
178
178
|
and for `publish` requires CI, version, npm integrity, and GitHub Release evidence. Receipts are
|
|
179
179
|
append-only in `.brain/runtime/delivery-receipts.jsonl`. If code/config must change, delivery
|
|
180
180
|
stops with `WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED` and work returns to implementation.
|
|
181
|
+
- In a multi-context Vault, `active_contexts[].delivery_id` is authoritative. `--session <id>`
|
|
182
|
+
selects the work session explicitly; without it, only one unambiguous active context for the
|
|
183
|
+
worktree may be used. Implicit status, finish, and abandon resolve that binding, not a global pointer.
|
|
184
|
+
- `CURRENT_DELIVERY` is only a derived projection: it contains the ID for one single, unambiguous
|
|
185
|
+
active context with delivery and stays empty with zero or multiple contexts.
|
|
186
|
+
`WENDKEEP_DELIVERY_CONTEXT_MISMATCH` means the explicit ID belongs to another context; neither
|
|
187
|
+
context is mutated.
|
|
188
|
+
- The `change-context` and `change-warn` hooks consult only the caller's causal delivery; one
|
|
189
|
+
session's authorization is neither injected into nor allowed to suppress warnings in another.
|
|
181
190
|
- Exit `0` means a successful query or transition; exit `1` means a policy/red-sensor block; exit
|
|
182
191
|
`2` means invalid profile, session, flow, or arguments, with no partial mutation.
|
|
183
192
|
|
|
@@ -25,12 +25,12 @@ Tenha o projeto inicializado, um vault saudável e `wendkeep.sensors.json` váli
|
|
|
25
25
|
## Sintaxe
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx wendkeep change new <slug> [--simple|--guide]
|
|
29
|
-
npx wendkeep change status [slug]
|
|
30
|
-
npx wendkeep spec effective --change <slug>
|
|
28
|
+
npx wendkeep change new <slug> [--simple|--guide] [--session <id>]
|
|
29
|
+
npx wendkeep change status [slug] [--session <id>]
|
|
30
|
+
npx wendkeep spec effective [--change <slug>] [--session <id>]
|
|
31
31
|
npx wendkeep sensors list
|
|
32
|
-
npx wendkeep verify [--deep] [--change <slug>]
|
|
33
|
-
npx wendkeep change archive <slug>
|
|
32
|
+
npx wendkeep verify [--deep] [--change <slug>] [--session <id>]
|
|
33
|
+
npx wendkeep change archive <slug> [--session <id>]
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Opções e códigos de saída
|
|
@@ -43,6 +43,8 @@ npx wendkeep change archive <slug>
|
|
|
43
43
|
trabalho sem arquivar.
|
|
44
44
|
- `change continue <arquivada> <nova>` abre continuação sem herdar evidência antiga.
|
|
45
45
|
- `change bind <slug> --session <id>` liga uma sessão existente.
|
|
46
|
+
- `--session <id>` seleciona o `active_contexts` causal nos comandos implícitos. Sem a opção,
|
|
47
|
+
somente um contexto ativo inequívoco da worktree é aceito; ambiguidade retorna exit `2`.
|
|
46
48
|
- `change relink [--apply]` e `change backlink [--apply]` reparam o grafo; dry-run é o padrão.
|
|
47
49
|
- `change abandon <slug>` descarta sem ADR; `archive --force` exige decisão humana explícita.
|
|
48
50
|
- `wendkeep spec list|show|effective|migrate|rebase` administra contratos vivos e deltas.
|
|
@@ -54,6 +56,7 @@ npx wendkeep change archive <slug>
|
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
58
|
npx wendkeep change new login-tenant
|
|
59
|
+
npx wendkeep change use login-tenant --session <id>
|
|
57
60
|
npx wendkeep change new ajuste-interno --guide
|
|
58
61
|
npx wendkeep spec effective --change login-tenant
|
|
59
62
|
npx wendkeep change done 1.1 --change login-tenant
|
|
@@ -82,6 +85,10 @@ uma ferramenta de escrita suportada, ele compara sessão, projeto, raiz Git, rem
|
|
|
82
85
|
worktree com a lease registrada no `SESSION_REGISTRY.json`. Um alvo ausente, ambíguo, concorrente ou
|
|
83
86
|
fora do projeto é bloqueado antes da ferramenta.
|
|
84
87
|
|
|
88
|
+
O foco implícito de change vem de `active_contexts`, não de `CURRENT_CHANGE.md`. A chave combina
|
|
89
|
+
`repository_id`, `worktree_id` e `work_session_id`; o ponteiro Markdown permanece apenas como
|
|
90
|
+
projeção compatível quando existe um único contexto inequívoco.
|
|
91
|
+
|
|
85
92
|
O [Observer local](observer.md) é uma projeção read-only da observabilidade: o vault e a change
|
|
86
93
|
continuam autoridades locais. Consultas do Observer não concluem, arquivam, reparam ou promovem
|
|
87
94
|
estado no vault.
|
|
@@ -82,6 +82,22 @@ atuais. Sob o lock do registry, o comando revalida a revisão, incrementa `conte
|
|
|
82
82
|
change/lease/autorizações, limpa somente a quarentena e anexa um receipt sanitizado em
|
|
83
83
|
`context_recoveries`. Qualquer falha deixa registry e quarentena byte a byte intactos.
|
|
84
84
|
|
|
85
|
+
### Registry multi-contexto de changes
|
|
86
|
+
|
|
87
|
+
O `SESSION_REGISTRY.json` mantém `active_contexts` com schema e revisão próprios. Cada entrada é
|
|
88
|
+
identificada por `repository_id` + `worktree_id` + `work_session_id`; branch, HEAD e `change_slug`
|
|
89
|
+
pertencem a essa entrada. Assim, duas worktrees podem selecionar changes diferentes sem
|
|
90
|
+
sobrescrever o foco uma da outra.
|
|
91
|
+
|
|
92
|
+
Com sessão causal explícita, change, spec e verify resolvem somente a entrada correspondente. Sem
|
|
93
|
+
sessão, uma única entrada ativa da worktree pode ser usada; duas sessões compatíveis causam
|
|
94
|
+
ambiguidade e a operação falha fechada, sem escolher uma change em silêncio.
|
|
95
|
+
|
|
96
|
+
`CURRENT_CHANGE.md` é apenas uma projeção derivada: contém a change somente quando existe um único
|
|
97
|
+
contexto ativo inequívoco. Com zero ou múltiplos contextos, fica vazio. A migração é conservadora:
|
|
98
|
+
não inventa uma identidade de worktree ou sessão. O ponteiro legado só vira contexto quando uma sessão
|
|
99
|
+
ativa, scope completa e metadados da worktree provam uma única identidade.
|
|
100
|
+
|
|
85
101
|
## Erros comuns e diagnóstico
|
|
86
102
|
|
|
87
103
|
- `WENDKEEP_CONTEXT_AMBIGUOUS`: informe `--session <id>`; nenhuma candidata é escolhida em silêncio.
|
|
@@ -57,10 +57,10 @@ npx wendkeep flow status [<id>]
|
|
|
57
57
|
npx wendkeep flow show <id> [--session <id>]
|
|
58
58
|
npx wendkeep flow finish <id> [--session <id>]
|
|
59
59
|
npx wendkeep flow promote <id> [--change-slug <slug>] [--session <id>]
|
|
60
|
-
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
61
|
-
npx wendkeep delivery status [id]
|
|
62
|
-
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>]
|
|
63
|
-
npx wendkeep delivery abandon [id] --reason <texto>
|
|
60
|
+
npx wendkeep delivery start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>] [--session <id>]
|
|
61
|
+
npx wendkeep delivery status [id] [--session <id>]
|
|
62
|
+
npx wendkeep delivery finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>] [--npm-integrity <sha512>] [--release-url <url>] [--session <id>]
|
|
63
|
+
npx wendkeep delivery abandon [id] --reason <texto> [--session <id>]
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Todos os subcomandos FLOW também aceitam `--project <path>`, `--vault <path>` e `--json`.
|
|
@@ -177,6 +177,15 @@ harness nativo da LLM.
|
|
|
177
177
|
para capability `publish`, exige CI, versão, integridade npm e GitHub Release. O receipt é
|
|
178
178
|
append-only em `.brain/runtime/delivery-receipts.jsonl`. Se código/config precisar mudar, a
|
|
179
179
|
delivery para com `WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED` e o trabalho volta a implementation.
|
|
180
|
+
- Em Vault multi-contexto, `active_contexts[].delivery_id` é a autoridade. `--session <id>` escolhe
|
|
181
|
+
explicitamente a work session; sem ela, somente um active context inequívoco da worktree pode ser
|
|
182
|
+
usado. Status, finish e abandon implícitos consultam esse binding, não um ponteiro global.
|
|
183
|
+
- `CURRENT_DELIVERY` é somente uma projeção derivada: contém o ID quando existe um único contexto
|
|
184
|
+
ativo inequívoco com delivery e fica vazio com zero ou múltiplos contextos.
|
|
185
|
+
`WENDKEEP_DELIVERY_CONTEXT_MISMATCH` indica que o ID explícito pertence a outro contexto; nenhum
|
|
186
|
+
dos dois é mutado.
|
|
187
|
+
- Os hooks `change-context` e `change-warn` consultam somente o delivery causal do chamador; a
|
|
188
|
+
autorização de uma sessão não é injetada nem suprime aviso em outra.
|
|
180
189
|
- Exit `0` indica consulta ou transição concluída; exit `1` indica política/sensor vermelho; exit
|
|
181
190
|
`2` indica perfil, sessão, flow ou argumentos inválidos, sem mutação parcial.
|
|
182
191
|
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { mutateSessionRegistry, readSessionRegistry } from './obsidian-common.mjs';
|
|
5
|
+
import { mkdirVaultPath, writeVaultFileSync } from './vault-path-safety.mjs';
|
|
6
|
+
|
|
7
|
+
export const ACTIVE_CONTEXTS_SCHEMA_VERSION = 1;
|
|
8
|
+
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
9
|
+
const DELIVERY_POINTER = '.brain/runtime/CURRENT_DELIVERY';
|
|
10
|
+
const ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
|
|
11
|
+
const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
|
|
12
|
+
const DELIVERY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,100}$/;
|
|
13
|
+
|
|
14
|
+
function contextError(code, message) {
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
error.code = code;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requiredId(value, label) {
|
|
21
|
+
const normalized = String(value || '').trim();
|
|
22
|
+
if (!ID_PATTERN.test(normalized)) {
|
|
23
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_INVALID', `${label} inválido ou ausente`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function optionalText(value, maxLength = 240) {
|
|
29
|
+
const normalized = String(value || '').trim();
|
|
30
|
+
return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeIdentity(identity = {}, { requireWorkSession = true } = {}) {
|
|
34
|
+
const workSessionId = requireWorkSession
|
|
35
|
+
? requiredId(identity.workSessionId ?? identity.work_session_id, 'work_session_id')
|
|
36
|
+
: optionalText(identity.workSessionId ?? identity.work_session_id, 160);
|
|
37
|
+
return {
|
|
38
|
+
projectId: requiredId(identity.projectId ?? identity.project_id, 'project_id'),
|
|
39
|
+
repositoryId: requiredId(identity.repositoryId ?? identity.repository_id, 'repository_id'),
|
|
40
|
+
worktreeId: requiredId(identity.worktreeId ?? identity.worktree_id, 'worktree_id'),
|
|
41
|
+
workSessionId,
|
|
42
|
+
branch: optionalText(identity.branch, 240),
|
|
43
|
+
headSha: optionalText(identity.headSha ?? identity.head_sha, 80),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function activeContextKey(identity) {
|
|
48
|
+
const normalized = normalizeIdentity(identity);
|
|
49
|
+
return `${normalized.repositoryId}:${normalized.worktreeId}:${normalized.workSessionId}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function contextsOf(registry) {
|
|
53
|
+
return registry?.active_contexts && typeof registry.active_contexts === 'object'
|
|
54
|
+
&& !Array.isArray(registry.active_contexts)
|
|
55
|
+
? registry.active_contexts : {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function currentGlobalRevision(registry) {
|
|
59
|
+
const value = Number(registry?.active_contexts_revision);
|
|
60
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentContextRevision(context) {
|
|
64
|
+
const value = Number(context?.revision);
|
|
65
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function activeContexts(registry) {
|
|
69
|
+
return Object.entries(contextsOf(registry)).filter(([, context]) => context?.state === 'active');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function legacyProjection(registry) {
|
|
73
|
+
const active = activeContexts(registry);
|
|
74
|
+
if (active.length !== 1) return '';
|
|
75
|
+
return String(active[0][1]?.change_slug || '').trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function projectLegacyActiveChange(vaultBase, registry = readSessionRegistry(vaultBase)) {
|
|
79
|
+
const slug = legacyProjection(registry);
|
|
80
|
+
mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz de controle da change' });
|
|
81
|
+
writeVaultFileSync(
|
|
82
|
+
vaultBase,
|
|
83
|
+
join(vaultBase, POINTER),
|
|
84
|
+
slug ? `change: ${slug}\n` : 'change:\n',
|
|
85
|
+
'utf8',
|
|
86
|
+
{ label: 'projeção legada CURRENT_CHANGE.md' },
|
|
87
|
+
);
|
|
88
|
+
return slug;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function projectLegacyActiveDelivery(vaultBase, registry = readSessionRegistry(vaultBase)) {
|
|
92
|
+
const active = activeContexts(registry);
|
|
93
|
+
const id = active.length === 1 ? optionalText(active[0][1]?.delivery_id, 101) : '';
|
|
94
|
+
mkdirVaultPath(vaultBase, join(vaultBase, '.brain', 'runtime'), {
|
|
95
|
+
label: 'runtime da projeção legada de delivery',
|
|
96
|
+
});
|
|
97
|
+
writeVaultFileSync(
|
|
98
|
+
vaultBase,
|
|
99
|
+
join(vaultBase, DELIVERY_POINTER),
|
|
100
|
+
id ? `${id}\n` : '',
|
|
101
|
+
'utf8',
|
|
102
|
+
{ label: 'projeção legada CURRENT_DELIVERY' },
|
|
103
|
+
);
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resolveActiveContext(vaultBase, query = {}) {
|
|
108
|
+
const normalized = normalizeIdentity(query, { requireWorkSession: false });
|
|
109
|
+
const registry = readSessionRegistry(vaultBase);
|
|
110
|
+
let workSessionId = normalized.workSessionId;
|
|
111
|
+
const sessionId = optionalText(query.sessionId ?? query.session_id, 160);
|
|
112
|
+
if (!workSessionId && sessionId) {
|
|
113
|
+
const entry = registry.sessions?.[sessionId];
|
|
114
|
+
if (entry?.status !== 'active' || !entry.work_session_id) {
|
|
115
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'sessão causal ativa não encontrada');
|
|
116
|
+
}
|
|
117
|
+
workSessionId = String(entry.work_session_id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const candidates = activeContexts(registry).filter(([, context]) => (
|
|
121
|
+
context.project_id === normalized.projectId
|
|
122
|
+
&& context.repository_id === normalized.repositoryId
|
|
123
|
+
&& context.worktree_id === normalized.worktreeId
|
|
124
|
+
&& (!workSessionId || context.work_session_id === workSessionId)
|
|
125
|
+
));
|
|
126
|
+
if (!candidates.length) {
|
|
127
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'active context não encontrado para a identidade informada');
|
|
128
|
+
}
|
|
129
|
+
if (candidates.length > 1) {
|
|
130
|
+
throw contextError(
|
|
131
|
+
'WENDKEEP_ACTIVE_CONTEXT_AMBIGUOUS',
|
|
132
|
+
'mais de um active context corresponde à worktree; informe a sessão causal',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return structuredClone(candidates[0][1]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function mutateActiveContext(vaultBase, identity, updater, {
|
|
139
|
+
expectedRevision,
|
|
140
|
+
now = new Date().toISOString(),
|
|
141
|
+
mutateRegistry = mutateSessionRegistry,
|
|
142
|
+
} = {}) {
|
|
143
|
+
const normalized = normalizeIdentity(identity);
|
|
144
|
+
const key = activeContextKey(normalized);
|
|
145
|
+
const result = mutateRegistry(vaultBase, (registry) => {
|
|
146
|
+
const contexts = contextsOf(registry);
|
|
147
|
+
const existing = contexts[key] || null;
|
|
148
|
+
const revision = currentContextRevision(existing);
|
|
149
|
+
if (expectedRevision !== undefined && Number(expectedRevision) !== revision) {
|
|
150
|
+
throw contextError(
|
|
151
|
+
'WENDKEEP_ACTIVE_CONTEXT_STALE',
|
|
152
|
+
`active context revision mudou de ${expectedRevision} para ${revision}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const base = existing || {
|
|
156
|
+
project_id: normalized.projectId,
|
|
157
|
+
repository_id: normalized.repositoryId,
|
|
158
|
+
worktree_id: normalized.worktreeId,
|
|
159
|
+
work_session_id: normalized.workSessionId,
|
|
160
|
+
branch: normalized.branch,
|
|
161
|
+
head_sha: normalized.headSha,
|
|
162
|
+
change_slug: '',
|
|
163
|
+
state: 'active',
|
|
164
|
+
revision: 0,
|
|
165
|
+
updated_at: String(now),
|
|
166
|
+
};
|
|
167
|
+
const updated = updater(structuredClone(base));
|
|
168
|
+
if (!updated || typeof updated !== 'object' || Array.isArray(updated)) {
|
|
169
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_INVALID', 'mutação retornou active context inválido');
|
|
170
|
+
}
|
|
171
|
+
for (const [field, expected] of [
|
|
172
|
+
['project_id', normalized.projectId],
|
|
173
|
+
['repository_id', normalized.repositoryId],
|
|
174
|
+
['worktree_id', normalized.worktreeId],
|
|
175
|
+
['work_session_id', normalized.workSessionId],
|
|
176
|
+
]) {
|
|
177
|
+
if (updated[field] !== expected) {
|
|
178
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', `${field} não pode mudar`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const next = {
|
|
182
|
+
...updated,
|
|
183
|
+
branch: normalized.branch || optionalText(updated.branch, 240),
|
|
184
|
+
head_sha: normalized.headSha || optionalText(updated.head_sha, 80),
|
|
185
|
+
state: updated.state === 'closed' ? 'closed' : 'active',
|
|
186
|
+
revision: revision + 1,
|
|
187
|
+
updated_at: String(now),
|
|
188
|
+
};
|
|
189
|
+
registry.active_contexts_schema = ACTIVE_CONTEXTS_SCHEMA_VERSION;
|
|
190
|
+
registry.active_contexts_revision = currentGlobalRevision(registry) + 1;
|
|
191
|
+
registry.active_contexts = { ...contexts, [key]: next };
|
|
192
|
+
return { key, context: structuredClone(next), registryRevision: registry.active_contexts_revision };
|
|
193
|
+
});
|
|
194
|
+
projectLegacyActiveChange(vaultBase);
|
|
195
|
+
projectLegacyActiveDelivery(vaultBase);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function setActiveContextChange(vaultBase, identity, slug, options = {}) {
|
|
200
|
+
const normalizedSlug = String(slug || '').trim();
|
|
201
|
+
if (!SLUG_PATTERN.test(normalizedSlug)) {
|
|
202
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_CHANGE_INVALID', 'change_slug inválido ou ausente');
|
|
203
|
+
}
|
|
204
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
205
|
+
...context,
|
|
206
|
+
change_slug: normalizedSlug,
|
|
207
|
+
state: 'active',
|
|
208
|
+
}), options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function clearActiveContextChange(vaultBase, identity, options = {}) {
|
|
212
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
213
|
+
...context,
|
|
214
|
+
change_slug: '',
|
|
215
|
+
state: 'active',
|
|
216
|
+
}), options);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function setActiveContextDelivery(vaultBase, identity, deliveryId, options = {}) {
|
|
220
|
+
const normalizedId = String(deliveryId || '').trim();
|
|
221
|
+
if (!DELIVERY_PATTERN.test(normalizedId)) {
|
|
222
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_DELIVERY_INVALID', 'delivery_id inválido ou ausente');
|
|
223
|
+
}
|
|
224
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
225
|
+
...context,
|
|
226
|
+
delivery_id: normalizedId,
|
|
227
|
+
state: 'active',
|
|
228
|
+
}), options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function clearActiveContextDelivery(vaultBase, identity, options = {}) {
|
|
232
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
233
|
+
...context,
|
|
234
|
+
delivery_id: '',
|
|
235
|
+
state: 'active',
|
|
236
|
+
}), options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function legacyPointer(vaultBase) {
|
|
240
|
+
try {
|
|
241
|
+
return readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
242
|
+
} catch {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function migrateLegacyActiveContext(vaultBase, {
|
|
248
|
+
identityForSession,
|
|
249
|
+
now = new Date().toISOString(),
|
|
250
|
+
} = {}) {
|
|
251
|
+
const slug = legacyPointer(vaultBase);
|
|
252
|
+
if (!slug) return { migrated: false, reason: 'no-pointer' };
|
|
253
|
+
const registry = readSessionRegistry(vaultBase);
|
|
254
|
+
if (Object.keys(contextsOf(registry)).length) return { migrated: false, reason: 'already-initialized' };
|
|
255
|
+
if (typeof identityForSession !== 'function') return { migrated: false, reason: 'identity-unavailable' };
|
|
256
|
+
|
|
257
|
+
const candidates = [];
|
|
258
|
+
for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
|
|
259
|
+
if (entry?.status !== 'active' || entry?.project_scope?.complete !== true || !entry?.work_session_id) continue;
|
|
260
|
+
let candidate;
|
|
261
|
+
try { candidate = normalizeIdentity(identityForSession(sessionId, entry)); } catch { continue; }
|
|
262
|
+
if (candidate.projectId !== entry.project_scope.projectId
|
|
263
|
+
|| candidate.workSessionId !== String(entry.work_session_id)) continue;
|
|
264
|
+
candidates.push(candidate);
|
|
265
|
+
}
|
|
266
|
+
if (candidates.length !== 1) {
|
|
267
|
+
return { migrated: false, reason: candidates.length ? 'ambiguous' : 'identity-unavailable' };
|
|
268
|
+
}
|
|
269
|
+
const result = setActiveContextChange(vaultBase, candidates[0], slug, { now });
|
|
270
|
+
return { migrated: true, key: result.key, context: result.context };
|
|
271
|
+
}
|
package/hooks/change-context.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveHookOperatingProfile,
|
|
16
16
|
} from './operating-profile-runtime.mjs';
|
|
17
17
|
import { activeDelivery } from '../src/delivery.mjs';
|
|
18
|
+
import { resolveCommandActiveContext } from '../src/active-context-runtime.mjs';
|
|
18
19
|
|
|
19
20
|
// Conservador de propósito: verbos de tarefa comuns (pt+en) + tamanho mínimo. Falso-negativo
|
|
20
21
|
// custa só o nudge; falso-positivo em pergunta curta viraria ruído.
|
|
@@ -26,11 +27,13 @@ export function looksLikeTask(prompt) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// Retorna { context, hash? } quando há algo a injetar; null = silêncio.
|
|
29
|
-
export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug = '', {
|
|
30
|
+
export function buildChangePing(vaultBase, sessionId, prompt = '', changeSlug = '', {
|
|
31
|
+
profile = 'GOVERN', context = null,
|
|
32
|
+
} = {}) {
|
|
30
33
|
const policy = hookProfilePolicy(profile);
|
|
31
34
|
if (!policy.harness) return null;
|
|
32
35
|
const sentinelId = profileSentinelId(sessionId, profile);
|
|
33
|
-
const delivery = activeDelivery(vaultBase);
|
|
36
|
+
const delivery = activeDelivery(vaultBase, { context });
|
|
34
37
|
if (delivery) {
|
|
35
38
|
const hash = `delivery:${delivery.id}`;
|
|
36
39
|
if (readSentinel(vaultBase, 'ctx', sentinelId) === hash) return null;
|
|
@@ -68,10 +71,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
68
71
|
const vaultBase = runtime.vaultBase;
|
|
69
72
|
const { identity, entry } = runtime;
|
|
70
73
|
const sid = identity.state === 'resolved' ? identity.canonicalConversationId : (input.session_id || input.sessionId || '');
|
|
74
|
+
const commandContext = runtime.bindingError ? null : resolveCommandActiveContext({
|
|
75
|
+
vaultBase,
|
|
76
|
+
projectRoot: input.cwd || process.cwd(),
|
|
77
|
+
sessionId: sid,
|
|
78
|
+
});
|
|
71
79
|
const ping = runtime.bindingError
|
|
72
80
|
? { context: profileRuntimeError(runtime.bindingError) }
|
|
73
81
|
: buildChangePing(vaultBase, sid, input.prompt || '', entry?.change_slug || '', {
|
|
74
82
|
profile: runtime.profile,
|
|
83
|
+
context: commandContext,
|
|
75
84
|
});
|
|
76
85
|
if (!ping) { writeHookOutput({}); }
|
|
77
86
|
else writeHookOutput({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: ping.context } });
|