wendkeep 0.76.3 → 0.76.4
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 +13 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/en/commands/changes-and-verification.md +12 -5
- package/docs/en/commands/context.md +16 -0
- package/docs/pt-BR/commands/changes-and-verification.md +12 -5
- package/docs/pt-BR/commands/context.md +16 -0
- package/hooks/active-context-store.mjs +232 -0
- package/hooks/change-core.mjs +35 -17
- 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/spec.mjs +15 -2
- package/src/verify.mjs +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.4] — 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Registry multi-contexto.** `active_contexts` mantém change, branch e HEAD por
|
|
12
|
+
`repository_id` + `worktree_id` + `work_session_id`, com schema, revisão, CAS e writes atômicos;
|
|
13
|
+
duas worktrees ou sessões deixam de sobrescrever o foco operacional umas das outras.
|
|
14
|
+
- **Resolução causal de change.** Change, spec e verify usam a sessão/worktree chamadora; ausência
|
|
15
|
+
de sessão só aceita um contexto inequívoco e qualquer ambiguidade falha fechado.
|
|
16
|
+
- **Compatibilidade conservadora.** `CURRENT_CHANGE.md` é somente projeção quando há um único
|
|
17
|
+
contexto ativo, e a migração legada não inventa worktree ou sessão quando a identidade não pode
|
|
18
|
+
ser provada.
|
|
19
|
+
|
|
7
20
|
## [0.76.3] — 2026-08-21
|
|
8
21
|
|
|
9
22
|
### 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) |
|
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) |
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -0,0 +1,232 @@
|
|
|
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 ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
|
|
10
|
+
const SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$/;
|
|
11
|
+
|
|
12
|
+
function contextError(code, message) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.code = code;
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requiredId(value, label) {
|
|
19
|
+
const normalized = String(value || '').trim();
|
|
20
|
+
if (!ID_PATTERN.test(normalized)) {
|
|
21
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_INVALID', `${label} inválido ou ausente`);
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalText(value, maxLength = 240) {
|
|
27
|
+
const normalized = String(value || '').trim();
|
|
28
|
+
return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeIdentity(identity = {}, { requireWorkSession = true } = {}) {
|
|
32
|
+
const workSessionId = requireWorkSession
|
|
33
|
+
? requiredId(identity.workSessionId ?? identity.work_session_id, 'work_session_id')
|
|
34
|
+
: optionalText(identity.workSessionId ?? identity.work_session_id, 160);
|
|
35
|
+
return {
|
|
36
|
+
projectId: requiredId(identity.projectId ?? identity.project_id, 'project_id'),
|
|
37
|
+
repositoryId: requiredId(identity.repositoryId ?? identity.repository_id, 'repository_id'),
|
|
38
|
+
worktreeId: requiredId(identity.worktreeId ?? identity.worktree_id, 'worktree_id'),
|
|
39
|
+
workSessionId,
|
|
40
|
+
branch: optionalText(identity.branch, 240),
|
|
41
|
+
headSha: optionalText(identity.headSha ?? identity.head_sha, 80),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function activeContextKey(identity) {
|
|
46
|
+
const normalized = normalizeIdentity(identity);
|
|
47
|
+
return `${normalized.repositoryId}:${normalized.worktreeId}:${normalized.workSessionId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function contextsOf(registry) {
|
|
51
|
+
return registry?.active_contexts && typeof registry.active_contexts === 'object'
|
|
52
|
+
&& !Array.isArray(registry.active_contexts)
|
|
53
|
+
? registry.active_contexts : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function currentGlobalRevision(registry) {
|
|
57
|
+
const value = Number(registry?.active_contexts_revision);
|
|
58
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function currentContextRevision(context) {
|
|
62
|
+
const value = Number(context?.revision);
|
|
63
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function activeContexts(registry) {
|
|
67
|
+
return Object.entries(contextsOf(registry)).filter(([, context]) => context?.state === 'active');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function legacyProjection(registry) {
|
|
71
|
+
const active = activeContexts(registry);
|
|
72
|
+
if (active.length !== 1) return '';
|
|
73
|
+
return String(active[0][1]?.change_slug || '').trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function projectLegacyActiveChange(vaultBase, registry = readSessionRegistry(vaultBase)) {
|
|
77
|
+
const slug = legacyProjection(registry);
|
|
78
|
+
mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz de controle da change' });
|
|
79
|
+
writeVaultFileSync(
|
|
80
|
+
vaultBase,
|
|
81
|
+
join(vaultBase, POINTER),
|
|
82
|
+
slug ? `change: ${slug}\n` : 'change:\n',
|
|
83
|
+
'utf8',
|
|
84
|
+
{ label: 'projeção legada CURRENT_CHANGE.md' },
|
|
85
|
+
);
|
|
86
|
+
return slug;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveActiveContext(vaultBase, query = {}) {
|
|
90
|
+
const normalized = normalizeIdentity(query, { requireWorkSession: false });
|
|
91
|
+
const registry = readSessionRegistry(vaultBase);
|
|
92
|
+
let workSessionId = normalized.workSessionId;
|
|
93
|
+
const sessionId = optionalText(query.sessionId ?? query.session_id, 160);
|
|
94
|
+
if (!workSessionId && sessionId) {
|
|
95
|
+
const entry = registry.sessions?.[sessionId];
|
|
96
|
+
if (entry?.status !== 'active' || !entry.work_session_id) {
|
|
97
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'sessão causal ativa não encontrada');
|
|
98
|
+
}
|
|
99
|
+
workSessionId = String(entry.work_session_id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const candidates = activeContexts(registry).filter(([, context]) => (
|
|
103
|
+
context.project_id === normalized.projectId
|
|
104
|
+
&& context.repository_id === normalized.repositoryId
|
|
105
|
+
&& context.worktree_id === normalized.worktreeId
|
|
106
|
+
&& (!workSessionId || context.work_session_id === workSessionId)
|
|
107
|
+
));
|
|
108
|
+
if (!candidates.length) {
|
|
109
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND', 'active context não encontrado para a identidade informada');
|
|
110
|
+
}
|
|
111
|
+
if (candidates.length > 1) {
|
|
112
|
+
throw contextError(
|
|
113
|
+
'WENDKEEP_ACTIVE_CONTEXT_AMBIGUOUS',
|
|
114
|
+
'mais de um active context corresponde à worktree; informe a sessão causal',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return structuredClone(candidates[0][1]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mutateActiveContext(vaultBase, identity, updater, {
|
|
121
|
+
expectedRevision,
|
|
122
|
+
now = new Date().toISOString(),
|
|
123
|
+
mutateRegistry = mutateSessionRegistry,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const normalized = normalizeIdentity(identity);
|
|
126
|
+
const key = activeContextKey(normalized);
|
|
127
|
+
const result = mutateRegistry(vaultBase, (registry) => {
|
|
128
|
+
const contexts = contextsOf(registry);
|
|
129
|
+
const existing = contexts[key] || null;
|
|
130
|
+
const revision = currentContextRevision(existing);
|
|
131
|
+
if (expectedRevision !== undefined && Number(expectedRevision) !== revision) {
|
|
132
|
+
throw contextError(
|
|
133
|
+
'WENDKEEP_ACTIVE_CONTEXT_STALE',
|
|
134
|
+
`active context revision mudou de ${expectedRevision} para ${revision}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const base = existing || {
|
|
138
|
+
project_id: normalized.projectId,
|
|
139
|
+
repository_id: normalized.repositoryId,
|
|
140
|
+
worktree_id: normalized.worktreeId,
|
|
141
|
+
work_session_id: normalized.workSessionId,
|
|
142
|
+
branch: normalized.branch,
|
|
143
|
+
head_sha: normalized.headSha,
|
|
144
|
+
change_slug: '',
|
|
145
|
+
state: 'active',
|
|
146
|
+
revision: 0,
|
|
147
|
+
updated_at: String(now),
|
|
148
|
+
};
|
|
149
|
+
const updated = updater(structuredClone(base));
|
|
150
|
+
if (!updated || typeof updated !== 'object' || Array.isArray(updated)) {
|
|
151
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_INVALID', 'mutação retornou active context inválido');
|
|
152
|
+
}
|
|
153
|
+
for (const [field, expected] of [
|
|
154
|
+
['project_id', normalized.projectId],
|
|
155
|
+
['repository_id', normalized.repositoryId],
|
|
156
|
+
['worktree_id', normalized.worktreeId],
|
|
157
|
+
['work_session_id', normalized.workSessionId],
|
|
158
|
+
]) {
|
|
159
|
+
if (updated[field] !== expected) {
|
|
160
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', `${field} não pode mudar`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const next = {
|
|
164
|
+
...updated,
|
|
165
|
+
branch: normalized.branch || optionalText(updated.branch, 240),
|
|
166
|
+
head_sha: normalized.headSha || optionalText(updated.head_sha, 80),
|
|
167
|
+
state: updated.state === 'closed' ? 'closed' : 'active',
|
|
168
|
+
revision: revision + 1,
|
|
169
|
+
updated_at: String(now),
|
|
170
|
+
};
|
|
171
|
+
registry.active_contexts_schema = ACTIVE_CONTEXTS_SCHEMA_VERSION;
|
|
172
|
+
registry.active_contexts_revision = currentGlobalRevision(registry) + 1;
|
|
173
|
+
registry.active_contexts = { ...contexts, [key]: next };
|
|
174
|
+
return { key, context: structuredClone(next), registryRevision: registry.active_contexts_revision };
|
|
175
|
+
});
|
|
176
|
+
projectLegacyActiveChange(vaultBase);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function setActiveContextChange(vaultBase, identity, slug, options = {}) {
|
|
181
|
+
const normalizedSlug = String(slug || '').trim();
|
|
182
|
+
if (!SLUG_PATTERN.test(normalizedSlug)) {
|
|
183
|
+
throw contextError('WENDKEEP_ACTIVE_CONTEXT_CHANGE_INVALID', 'change_slug inválido ou ausente');
|
|
184
|
+
}
|
|
185
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
186
|
+
...context,
|
|
187
|
+
change_slug: normalizedSlug,
|
|
188
|
+
state: 'active',
|
|
189
|
+
}), options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function clearActiveContextChange(vaultBase, identity, options = {}) {
|
|
193
|
+
return mutateActiveContext(vaultBase, identity, (context) => ({
|
|
194
|
+
...context,
|
|
195
|
+
change_slug: '',
|
|
196
|
+
state: 'active',
|
|
197
|
+
}), options);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function legacyPointer(vaultBase) {
|
|
201
|
+
try {
|
|
202
|
+
return readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
203
|
+
} catch {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function migrateLegacyActiveContext(vaultBase, {
|
|
209
|
+
identityForSession,
|
|
210
|
+
now = new Date().toISOString(),
|
|
211
|
+
} = {}) {
|
|
212
|
+
const slug = legacyPointer(vaultBase);
|
|
213
|
+
if (!slug) return { migrated: false, reason: 'no-pointer' };
|
|
214
|
+
const registry = readSessionRegistry(vaultBase);
|
|
215
|
+
if (Object.keys(contextsOf(registry)).length) return { migrated: false, reason: 'already-initialized' };
|
|
216
|
+
if (typeof identityForSession !== 'function') return { migrated: false, reason: 'identity-unavailable' };
|
|
217
|
+
|
|
218
|
+
const candidates = [];
|
|
219
|
+
for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
|
|
220
|
+
if (entry?.status !== 'active' || entry?.project_scope?.complete !== true || !entry?.work_session_id) continue;
|
|
221
|
+
let candidate;
|
|
222
|
+
try { candidate = normalizeIdentity(identityForSession(sessionId, entry)); } catch { continue; }
|
|
223
|
+
if (candidate.projectId !== entry.project_scope.projectId
|
|
224
|
+
|| candidate.workSessionId !== String(entry.work_session_id)) continue;
|
|
225
|
+
candidates.push(candidate);
|
|
226
|
+
}
|
|
227
|
+
if (candidates.length !== 1) {
|
|
228
|
+
return { migrated: false, reason: candidates.length ? 'ambiguous' : 'identity-unavailable' };
|
|
229
|
+
}
|
|
230
|
+
const result = setActiveContextChange(vaultBase, candidates[0], slug, { now });
|
|
231
|
+
return { migrated: true, key: result.key, context: result.context };
|
|
232
|
+
}
|
package/hooks/change-core.mjs
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, renameVaultPath,
|
|
11
11
|
unlinkVaultFile, writeVaultFileSync,
|
|
12
12
|
} from './vault-path-safety.mjs';
|
|
13
|
+
import {
|
|
14
|
+
clearActiveContextChange,
|
|
15
|
+
resolveActiveContext,
|
|
16
|
+
setActiveContextChange,
|
|
17
|
+
} from './active-context-store.mjs';
|
|
13
18
|
|
|
14
19
|
export const ARCHIVE_DIR = '_arquivo';
|
|
15
20
|
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
@@ -118,7 +123,14 @@ export function scaffoldPlaceholders(dir) {
|
|
|
118
123
|
return found;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
export function activeChange(vaultBase) {
|
|
126
|
+
export function activeChange(vaultBase, { context } = {}) {
|
|
127
|
+
if (context) {
|
|
128
|
+
try { return String(resolveActiveContext(vaultBase, context).change_slug || '').trim(); }
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') return '';
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
try {
|
|
123
135
|
const m = readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m);
|
|
124
136
|
return m ? m[1].trim() : '';
|
|
@@ -127,7 +139,8 @@ export function activeChange(vaultBase) {
|
|
|
127
139
|
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
export function setActiveChange(vaultBase, slug) {
|
|
142
|
+
export function setActiveChange(vaultBase, slug, { context } = {}) {
|
|
143
|
+
if (context) return setActiveContextChange(vaultBase, context, slug);
|
|
131
144
|
mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz de controle da change' });
|
|
132
145
|
writeVaultFileSync(
|
|
133
146
|
vaultBase,
|
|
@@ -138,7 +151,8 @@ export function setActiveChange(vaultBase, slug) {
|
|
|
138
151
|
);
|
|
139
152
|
}
|
|
140
153
|
|
|
141
|
-
export function clearActiveChange(vaultBase) {
|
|
154
|
+
export function clearActiveChange(vaultBase, { context } = {}) {
|
|
155
|
+
if (context) return clearActiveContextChange(vaultBase, context);
|
|
142
156
|
const p = join(vaultBase, POINTER);
|
|
143
157
|
const checked = assertVaultPathSafe(vaultBase, p, {
|
|
144
158
|
expectedType: 'file', label: 'ponteiro CURRENT_CHANGE.md',
|
|
@@ -191,7 +205,9 @@ export function assertChangeScaffoldTargetsSafe(vaultBase, slug, {
|
|
|
191
205
|
return { dir, rel: changeDirRel(slug, vaultBase) };
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
export function newChange(vaultBase, slug, {
|
|
208
|
+
export function newChange(vaultBase, slug, {
|
|
209
|
+
sessionRel = '', dateStr, simple = false, guide = false, context,
|
|
210
|
+
}) {
|
|
195
211
|
const compact = simple || guide;
|
|
196
212
|
const loc = getLocale(vaultBase);
|
|
197
213
|
const { dir } = assertChangeScaffoldTargetsSafe(vaultBase, slug, { simple: compact });
|
|
@@ -218,14 +234,14 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
218
234
|
write('design.md', files.design);
|
|
219
235
|
}
|
|
220
236
|
if (!existed) captureSpecBaseline(vaultBase, dir);
|
|
221
|
-
setActiveChange(vaultBase, slug);
|
|
237
|
+
setActiveChange(vaultBase, slug, { context });
|
|
222
238
|
return { rel: changeDirRel(slug, vaultBase), created: !existed };
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
export function useChange(vaultBase, slug) {
|
|
241
|
+
export function useChange(vaultBase, slug, { context } = {}) {
|
|
226
242
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
227
243
|
if (!existsSync(join(dir, 'proposta.md'))) return { ok: false, error: `change aberta não encontrada: ${slug}` };
|
|
228
|
-
setActiveChange(vaultBase, slug);
|
|
244
|
+
setActiveChange(vaultBase, slug, { context });
|
|
229
245
|
return { ok: true, rel: changeDirRel(slug, vaultBase) };
|
|
230
246
|
}
|
|
231
247
|
|
|
@@ -314,8 +330,8 @@ export function listChanges(vaultBase) {
|
|
|
314
330
|
// comandos implícitos; provider/session nunca filtram a fila, para que outro agente possa assumir
|
|
315
331
|
// o trabalho. O hash leva o conteúdo inteiro de cada tarefas.md — não apenas as contagens — pois
|
|
316
332
|
// ele controla a reinjeção por sessão dos hooks.
|
|
317
|
-
export function allChangesState(vaultBase) {
|
|
318
|
-
const current = activeChange(vaultBase);
|
|
333
|
+
export function allChangesState(vaultBase, { context } = {}) {
|
|
334
|
+
const current = activeChange(vaultBase, { context });
|
|
319
335
|
const { active } = listChanges(vaultBase);
|
|
320
336
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
321
337
|
const fingerprint = [`current:${current}`];
|
|
@@ -407,16 +423,16 @@ export function buildActiveChangeInjection(vaultBase, options = {}) {
|
|
|
407
423
|
return renderOpenChanges(allChangesState(vaultBase), options);
|
|
408
424
|
}
|
|
409
425
|
|
|
410
|
-
export function activeChangeLink(vaultBase) {
|
|
411
|
-
const slug = activeChange(vaultBase);
|
|
426
|
+
export function activeChangeLink(vaultBase, { context } = {}) {
|
|
427
|
+
const slug = activeChange(vaultBase, { context });
|
|
412
428
|
return slug ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${slug}/proposta]]` : '';
|
|
413
429
|
}
|
|
414
430
|
|
|
415
431
|
// --- Estado rápido do gate + sentinelas por sessão (0.31.0) --------------------
|
|
416
432
|
// Fonte única e barata (só leituras, tudo fail-open) do estado do gate, consumida pelos hooks
|
|
417
433
|
// de lifecycle (change-guard/change-nag/change-context) e pelo CLI. null sem change ativa.
|
|
418
|
-
export function quickGateState(vaultBase) {
|
|
419
|
-
const slug = activeChange(vaultBase);
|
|
434
|
+
export function quickGateState(vaultBase, { context } = {}) {
|
|
435
|
+
const slug = activeChange(vaultBase, { context });
|
|
420
436
|
if (!slug) return null;
|
|
421
437
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
422
438
|
let tarefasMd = '';
|
|
@@ -536,7 +552,9 @@ export function gateGreen() {
|
|
|
536
552
|
return { ok: true, failing: [] };
|
|
537
553
|
}
|
|
538
554
|
|
|
539
|
-
export function archiveChange(vaultBase, slug, {
|
|
555
|
+
export function archiveChange(vaultBase, slug, {
|
|
556
|
+
gate = gateGreen, dateStr, adrNum, adrFlags = {}, context,
|
|
557
|
+
}) {
|
|
540
558
|
const loc = getLocale(vaultBase);
|
|
541
559
|
const chDir = loc.folders.changes;
|
|
542
560
|
const src = join(vaultBase, chDir, slug);
|
|
@@ -656,7 +674,7 @@ Mudança ${changeWikilink} concluída e arquivada.${capLine}${reqLine}${forcedNo
|
|
|
656
674
|
|
|
657
675
|
// Only clear the pointer when the archived change IS the active one — archiving some other
|
|
658
676
|
// slug explicitly must not blank the pointer of a different, still-active change.
|
|
659
|
-
if (activeChange(vaultBase) === slug) clearActiveChange(vaultBase);
|
|
677
|
+
if (activeChange(vaultBase, { context }) === slug) clearActiveChange(vaultBase, { context });
|
|
660
678
|
return { ok: true, failing: [], archivedRel: destRel, adrRel: createAdr ? adrRel : '', promoted, specWarnings, linksRewritten };
|
|
661
679
|
}
|
|
662
680
|
|
|
@@ -849,7 +867,7 @@ export function relinkChanges(vaultBase, { apply = false } = {}) {
|
|
|
849
867
|
// Abandono (0.31.0): a saída legítima para uma change que não vai adiante — o que antes só o
|
|
850
868
|
// `archive --force` "resolvia", minting um ADR falso. Sem ADR, sem promoteSpecs (abandono não é
|
|
851
869
|
// decisão arquitetural nem promove contrato); move para _arquivo com sufixo -abandonada.
|
|
852
|
-
export function abandonChange(vaultBase, slug, { dateStr }) {
|
|
870
|
+
export function abandonChange(vaultBase, slug, { dateStr, context } = {}) {
|
|
853
871
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
854
872
|
const src = join(vaultBase, chDir, slug);
|
|
855
873
|
const checkedProposal = assertVaultPathSafe(vaultBase, join(src, 'proposta.md'), {
|
|
@@ -882,6 +900,6 @@ export function abandonChange(vaultBase, slug, { dateStr }) {
|
|
|
882
900
|
} catch { /* proposta sem frontmatter — segue */ }
|
|
883
901
|
let linksRewritten = 0;
|
|
884
902
|
try { linksRewritten = rewriteChangeLinks(vaultBase, `${chDir}/${slug}`, destRel.replaceAll('\\', '/')); } catch { /* abandono já íntegro */ }
|
|
885
|
-
if (activeChange(vaultBase) === slug) clearActiveChange(vaultBase);
|
|
903
|
+
if (activeChange(vaultBase, { context }) === slug) clearActiveChange(vaultBase, { context });
|
|
886
904
|
return { ok: true, failing: [], archivedRel: destRel, linksRewritten };
|
|
887
905
|
}
|
|
@@ -276,9 +276,12 @@ export function readSessionRegistry(vaultBase) {
|
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
278
|
const parsed = JSON.parse(readFileSync(checked.target, 'utf-8'));
|
|
279
|
+
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
279
280
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
...root,
|
|
282
|
+
version: Math.max(2, root.version || 1),
|
|
283
|
+
sessions: root.sessions && typeof root.sessions === 'object' && !Array.isArray(root.sessions)
|
|
284
|
+
? root.sessions : {},
|
|
282
285
|
};
|
|
283
286
|
} catch {
|
|
284
287
|
return { version: 2, sessions: {} };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.4",
|
|
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": [
|
|
@@ -64,6 +64,7 @@ Usage:
|
|
|
64
64
|
Inspect or explicitly recover a quarantined causal scope conflict.
|
|
65
65
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
66
66
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
67
|
+
--session <id> selects the causal active_context for implicit change operations.
|
|
67
68
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
68
69
|
backlink [--apply]: injeta o backlink pro proposta em design/tarefas/spec órfãos (open + _arquivo).
|
|
69
70
|
wendkeep theme sync Re-aplica o color system (snippet CSS + graph color groups) num vault
|
|
@@ -76,7 +77,7 @@ Usage:
|
|
|
76
77
|
FLOW records scope, sensors and a receipt without creating a change.
|
|
77
78
|
wendkeep delivery <sub> Operational delivery: start | status | finish | abandon.
|
|
78
79
|
Records authorization and an append-only receipt; never creates a change/spec/ADR.
|
|
79
|
-
wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
|
|
80
|
+
wendkeep spec <sub> Specs: list | show | effective [--change] [--session] [--json] | migrate | rebase.
|
|
80
81
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
81
82
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
82
83
|
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
@@ -91,7 +92,7 @@ Usage:
|
|
|
91
92
|
--rescan-decisions (capture prose decisions from already-imported transcripts) ·
|
|
92
93
|
--from <dir> · --codex-from <dir> · --since <date> · --limit N ·
|
|
93
94
|
--dry-run · --json.
|
|
94
|
-
wendkeep verify [--deep] [--change s] Run a change's task sensors + record evidence (the gate);
|
|
95
|
+
wendkeep verify [--deep] [--change s] [--session id] Run a change's task sensors + record evidence (the gate);
|
|
95
96
|
--deep assembles the verification package for the wk-verify pass.
|
|
96
97
|
wendkeep dashboard [--force] (Re)generate the vault's folder-filtered Bases + 00-Dashboard MOC.
|
|
97
98
|
wendkeep renumber-decisions Renumber 04-Decisões to ADR-<NNNN>-<slug> in chronological order,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
2
|
+
import { migrateLegacyActiveContext } from '../hooks/active-context-store.mjs';
|
|
3
|
+
import { captureProjectScope, compareProjectScopes } from '../hooks/project-scope.mjs';
|
|
4
|
+
import {
|
|
5
|
+
discoverWorktreeRepository,
|
|
6
|
+
readWorktreeRegistry,
|
|
7
|
+
worktreeIdentity,
|
|
8
|
+
} from '../packages/vault/src/worktree-metadata.mjs';
|
|
9
|
+
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
10
|
+
|
|
11
|
+
function runtimeError(code, message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function activeSession(entry) {
|
|
18
|
+
return entry?.status === 'active'
|
|
19
|
+
&& entry?.project_scope?.complete === true
|
|
20
|
+
&& typeof entry?.work_session_id === 'string'
|
|
21
|
+
&& entry.work_session_id.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveRuntimeActiveContext({
|
|
25
|
+
vaultBase,
|
|
26
|
+
projectRoot = process.cwd(),
|
|
27
|
+
sessionId = '',
|
|
28
|
+
spawn,
|
|
29
|
+
} = {}) {
|
|
30
|
+
const project = readProjectForValidation(vaultBase);
|
|
31
|
+
if (!project.ok || !project.projectId) {
|
|
32
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE', 'PROJECT.json não prova project_id');
|
|
33
|
+
}
|
|
34
|
+
let repository;
|
|
35
|
+
try { repository = discoverWorktreeRepository({ startDir: projectRoot, ...(spawn ? { spawn } : {}) }); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE', error.message);
|
|
38
|
+
}
|
|
39
|
+
const metadata = readWorktreeRegistry(repository).registry;
|
|
40
|
+
if (!metadata) {
|
|
41
|
+
throw runtimeError(
|
|
42
|
+
'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE',
|
|
43
|
+
'registry de worktrees ausente; não é seguro inventar repository_id/worktree_id',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (metadata.projectId !== project.projectId) {
|
|
47
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', 'registry de worktrees pertence a outro projeto');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const registry = readSessionRegistry(vaultBase);
|
|
51
|
+
const sessions = registry.sessions || {};
|
|
52
|
+
const worktreeId = worktreeIdentity(metadata.repositoryId, repository.gitDir);
|
|
53
|
+
const requested = String(sessionId || '').trim();
|
|
54
|
+
const rows = requested ? [[requested, sessions[requested]]] : Object.entries(sessions);
|
|
55
|
+
const matches = [];
|
|
56
|
+
for (const [candidateId, entry] of rows) {
|
|
57
|
+
if (!activeSession(entry)) continue;
|
|
58
|
+
const actual = captureProjectScope({
|
|
59
|
+
input: { cwd: projectRoot },
|
|
60
|
+
projectRoot,
|
|
61
|
+
projectId: project.projectId,
|
|
62
|
+
provider: entry.provider || '',
|
|
63
|
+
sessionId: candidateId,
|
|
64
|
+
...(spawn ? { spawn } : {}),
|
|
65
|
+
});
|
|
66
|
+
if (!compareProjectScopes(entry.project_scope, actual).ok) continue;
|
|
67
|
+
matches.push({ sessionId: candidateId, entry, actual });
|
|
68
|
+
}
|
|
69
|
+
if (!matches.length) {
|
|
70
|
+
throw runtimeError(
|
|
71
|
+
'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
|
|
72
|
+
requested ? 'sessão causal não corresponde à worktree atual' : 'nenhuma sessão ativa corresponde à worktree atual',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
let selected = matches[0];
|
|
76
|
+
if (matches.length > 1) {
|
|
77
|
+
const active = Object.values(registry.active_contexts || {}).filter((context) => (
|
|
78
|
+
context?.state === 'active'
|
|
79
|
+
&& context.project_id === project.projectId
|
|
80
|
+
&& context.repository_id === metadata.repositoryId
|
|
81
|
+
&& context.worktree_id === worktreeId
|
|
82
|
+
));
|
|
83
|
+
const narrowed = active.length === 1
|
|
84
|
+
? matches.filter(({ entry }) => String(entry.work_session_id) === active[0].work_session_id)
|
|
85
|
+
: [];
|
|
86
|
+
if (narrowed.length !== 1) {
|
|
87
|
+
throw runtimeError(
|
|
88
|
+
'WENDKEEP_ACTIVE_CONTEXT_AMBIGUOUS',
|
|
89
|
+
'mais de uma sessão ativa corresponde à worktree; informe --session',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
[selected] = narrowed;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
projectId: project.projectId,
|
|
96
|
+
repositoryId: metadata.repositoryId,
|
|
97
|
+
worktreeId,
|
|
98
|
+
workSessionId: String(selected.entry.work_session_id),
|
|
99
|
+
branch: selected.actual.branch,
|
|
100
|
+
headSha: selected.actual.head,
|
|
101
|
+
sessionId: selected.sessionId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveCommandActiveContext({
|
|
106
|
+
vaultBase,
|
|
107
|
+
projectRoot = process.cwd(),
|
|
108
|
+
sessionId = '',
|
|
109
|
+
spawn,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const requestedSession = String(sessionId || '').trim();
|
|
112
|
+
let identity;
|
|
113
|
+
try {
|
|
114
|
+
identity = resolveRuntimeActiveContext({
|
|
115
|
+
vaultBase, projectRoot, sessionId: requestedSession, ...(spawn ? { spawn } : {}),
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const registry = readSessionRegistry(vaultBase);
|
|
119
|
+
const initialized = Object.keys(registry.active_contexts || {}).length > 0;
|
|
120
|
+
const legacyUnavailable = error?.code === 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE'
|
|
121
|
+
|| (!requestedSession && error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND');
|
|
122
|
+
if (!initialized && legacyUnavailable) return null;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
migrateLegacyActiveContext(vaultBase, {
|
|
126
|
+
identityForSession: (candidateId) => resolveRuntimeActiveContext({
|
|
127
|
+
vaultBase, projectRoot, sessionId: candidateId, ...(spawn ? { spawn } : {}),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
return identity;
|
|
131
|
+
}
|
package/src/change.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
backfillArtifactLinks,
|
|
18
18
|
scaffoldPlaceholders,
|
|
19
19
|
isGuideCompactChange,
|
|
20
|
+
setActiveChange,
|
|
20
21
|
} from '../hooks/change-core.mjs';
|
|
21
22
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
22
23
|
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
@@ -24,6 +25,7 @@ import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegist
|
|
|
24
25
|
import { getLocale } from '../hooks/locale.mjs';
|
|
25
26
|
import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
|
|
26
27
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
28
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
27
29
|
|
|
28
30
|
function observerMarkdownUnder(vaultBase, relativeRoot) {
|
|
29
31
|
const output = [];
|
|
@@ -71,6 +73,24 @@ export function runChange(argv) {
|
|
|
71
73
|
const vaultBase = resolveVault(rest);
|
|
72
74
|
const VALUE_FLAGS = new Set(['--vault', '--change', '--project', '--session']);
|
|
73
75
|
const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
76
|
+
const projectRoot = resolve(opt(rest, '--project') || process.cwd());
|
|
77
|
+
let contextResolved = false;
|
|
78
|
+
let resolvedContext = null;
|
|
79
|
+
const context = () => {
|
|
80
|
+
if (contextResolved) return resolvedContext;
|
|
81
|
+
contextResolved = true;
|
|
82
|
+
const sessionId = opt(rest, '--session')
|
|
83
|
+
|| process.env.CODEX_THREAD_ID
|
|
84
|
+
|| process.env.CLAUDE_SESSION_ID
|
|
85
|
+
|| '';
|
|
86
|
+
try {
|
|
87
|
+
resolvedContext = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId });
|
|
88
|
+
return resolvedContext;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
process.stderr.write(`wendkeep change: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
74
94
|
|
|
75
95
|
if (sub === 'new') {
|
|
76
96
|
const slug = slugArg();
|
|
@@ -80,6 +100,7 @@ export function runChange(argv) {
|
|
|
80
100
|
try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* sem control */ }
|
|
81
101
|
const r = newChange(vaultBase, slug, {
|
|
82
102
|
dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
|
|
103
|
+
context: context(),
|
|
83
104
|
});
|
|
84
105
|
process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
|
|
85
106
|
process.exit(0);
|
|
@@ -88,7 +109,7 @@ export function runChange(argv) {
|
|
|
88
109
|
if (sub === 'use') {
|
|
89
110
|
const slug = slugArg();
|
|
90
111
|
if (!slug) { process.stderr.write('wendkeep change use: missing <slug>\n'); process.exit(2); }
|
|
91
|
-
const r = useChange(vaultBase, slug);
|
|
112
|
+
const r = useChange(vaultBase, slug, { context: context() });
|
|
92
113
|
if (!r.ok) { process.stderr.write(`wendkeep change use: ${r.error}\n`); process.exit(2); }
|
|
93
114
|
process.stdout.write(`current change: ${slug}\n`);
|
|
94
115
|
process.exit(0);
|
|
@@ -102,6 +123,8 @@ export function runChange(argv) {
|
|
|
102
123
|
if (!state.changes.some((item) => item.slug === slug)) { process.stderr.write(`wendkeep change bind: open change not found: ${slug}\n`); process.exit(2); }
|
|
103
124
|
if (!readSessionRegistry(vaultBase).sessions?.[sessionId]) { process.stderr.write(`wendkeep change bind: session not found: ${sessionId}\n`); process.exit(2); }
|
|
104
125
|
upsertSessionRegistry(vaultBase, sessionId, { change_slug: slug });
|
|
126
|
+
const selectedContext = context();
|
|
127
|
+
if (selectedContext) setActiveChange(vaultBase, slug, { context: selectedContext });
|
|
105
128
|
process.stdout.write(`session ${sessionId} -> change ${slug}\n`);
|
|
106
129
|
process.exit(0);
|
|
107
130
|
}
|
|
@@ -117,6 +140,7 @@ export function runChange(argv) {
|
|
|
117
140
|
try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* no control */ }
|
|
118
141
|
const r = continueChange(vaultBase, archivedSlug, newSlug, {
|
|
119
142
|
dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
|
|
143
|
+
context: context(),
|
|
120
144
|
});
|
|
121
145
|
if (!r.ok) { process.stderr.write(`wendkeep change continue: ${r.error}\n`); process.exit(2); }
|
|
122
146
|
process.stdout.write(`change created: ${r.rel} (continues ${r.archived}; active)\n`);
|
|
@@ -124,7 +148,7 @@ export function runChange(argv) {
|
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
if (sub === 'list') {
|
|
127
|
-
const state = allChangesState(vaultBase);
|
|
151
|
+
const state = allChangesState(vaultBase, { context: context() });
|
|
128
152
|
const { archived } = listChanges(vaultBase);
|
|
129
153
|
process.stdout.write(`${renderOpenChanges(state, { tag: '' }) || 'open changes: (none)'}\n`);
|
|
130
154
|
process.stdout.write(`archived: ${archived.join(', ') || '(none)'}\n`);
|
|
@@ -147,7 +171,7 @@ export function runChange(argv) {
|
|
|
147
171
|
if (sub === 'status') {
|
|
148
172
|
const slug = slugArg();
|
|
149
173
|
if (!slug) {
|
|
150
|
-
const state = allChangesState(vaultBase);
|
|
174
|
+
const state = allChangesState(vaultBase, { context: context() });
|
|
151
175
|
if (!state.changes.length && !state.pointerWarning) {
|
|
152
176
|
process.stderr.write('wendkeep change status: no open changes\n');
|
|
153
177
|
process.exit(2);
|
|
@@ -161,7 +185,7 @@ export function runChange(argv) {
|
|
|
161
185
|
catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
|
|
162
186
|
const tasks = parseTasks(tarefasMd);
|
|
163
187
|
const done = tasks.filter((t) => t.done).length;
|
|
164
|
-
process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase) ? ' (ativa)' : ''}\n`);
|
|
188
|
+
process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase, { context: context() }) ? ' (ativa)' : ''}\n`);
|
|
165
189
|
let specs = [];
|
|
166
190
|
try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); } catch { /* sem proposta */ }
|
|
167
191
|
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
@@ -194,7 +218,7 @@ export function runChange(argv) {
|
|
|
194
218
|
if (sub === 'done' || sub === 'undone') {
|
|
195
219
|
const taskId = slugArg();
|
|
196
220
|
if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
|
|
197
|
-
const slug = opt(rest, '--change') || activeChange(vaultBase);
|
|
221
|
+
const slug = opt(rest, '--change') || activeChange(vaultBase, { context: context() });
|
|
198
222
|
if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
|
|
199
223
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
200
224
|
let ok = false;
|
|
@@ -205,7 +229,7 @@ export function runChange(argv) {
|
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
if (sub === 'diff') {
|
|
208
|
-
const slug = slugArg() || activeChange(vaultBase);
|
|
232
|
+
const slug = slugArg() || activeChange(vaultBase, { context: context() });
|
|
209
233
|
if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
|
|
210
234
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
211
235
|
let specs = [];
|
|
@@ -228,7 +252,8 @@ export function runChange(argv) {
|
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
if (sub === 'archive') {
|
|
231
|
-
const
|
|
255
|
+
const selectedContext = context();
|
|
256
|
+
const slug = slugArg() || activeChange(vaultBase, { context: selectedContext });
|
|
232
257
|
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
233
258
|
// Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
|
|
234
259
|
const gate = (dir) => {
|
|
@@ -310,7 +335,10 @@ export function runChange(argv) {
|
|
|
310
335
|
if (trivial) process.stderr.write(compactGuide
|
|
311
336
|
? 'aviso: GUIDE compacta sem [req:]/[sensor:] — resultado permanece auditável no archive, sem ADR automático\n'
|
|
312
337
|
: 'aviso: change trivial (sem [req:]/[sensor:]) — ADR marcado trivial: true\n');
|
|
313
|
-
const r = archiveChange(vaultBase, slug, {
|
|
338
|
+
const r = archiveChange(vaultBase, slug, {
|
|
339
|
+
dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate,
|
|
340
|
+
adrFlags: { forced, trivial }, context: selectedContext,
|
|
341
|
+
});
|
|
314
342
|
if (!r.ok) {
|
|
315
343
|
process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
|
|
316
344
|
process.exit(1);
|
|
@@ -366,9 +394,10 @@ export function runChange(argv) {
|
|
|
366
394
|
}
|
|
367
395
|
|
|
368
396
|
if (sub === 'abandon') {
|
|
369
|
-
const
|
|
397
|
+
const selectedContext = context();
|
|
398
|
+
const slug = slugArg() || activeChange(vaultBase, { context: selectedContext });
|
|
370
399
|
if (!slug) { process.stderr.write('wendkeep change abandon: missing <slug> and no active change\n'); process.exit(2); }
|
|
371
|
-
const r = abandonChange(vaultBase, slug, { dateStr: today() });
|
|
400
|
+
const r = abandonChange(vaultBase, slug, { dateStr: today(), context: selectedContext });
|
|
372
401
|
if (!r.ok) { process.stderr.write(`wendkeep change abandon: ${r.failing.join('; ')}\n`); process.exit(2); }
|
|
373
402
|
process.stdout.write(`abandoned: ${r.archivedRel}\n`);
|
|
374
403
|
process.exit(0);
|
package/src/spec.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../hooks/spec-core.mjs';
|
|
12
12
|
import { activeChange, parseTasks } from '../hooks/change-core.mjs';
|
|
13
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
14
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
14
15
|
|
|
15
16
|
function resolveVault(argv) {
|
|
16
17
|
let vault;
|
|
@@ -38,9 +39,21 @@ export function runSpec(argv) {
|
|
|
38
39
|
const entry = rest.find((a) => a.startsWith(`${name}=`));
|
|
39
40
|
return entry ? entry.slice(name.length + 1) : undefined;
|
|
40
41
|
};
|
|
42
|
+
const commandContext = () => {
|
|
43
|
+
try {
|
|
44
|
+
return resolveCommandActiveContext({
|
|
45
|
+
vaultBase,
|
|
46
|
+
projectRoot: resolve(option('--project') || process.cwd()),
|
|
47
|
+
sessionId: option('--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
process.stderr.write(`wendkeep spec: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
41
54
|
|
|
42
55
|
if (sub === 'effective') {
|
|
43
|
-
const slug = option('--change') || activeChange(vaultBase);
|
|
56
|
+
const slug = option('--change') || activeChange(vaultBase, { context: commandContext() });
|
|
44
57
|
if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
|
|
45
58
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
46
59
|
let tasks = [];
|
|
@@ -74,7 +87,7 @@ export function runSpec(argv) {
|
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
if (sub === 'rebase') {
|
|
77
|
-
const slug = option('--change') || activeChange(vaultBase);
|
|
90
|
+
const slug = option('--change') || activeChange(vaultBase, { context: commandContext() });
|
|
78
91
|
if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
|
|
79
92
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
80
93
|
try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
|
package/src/verify.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../hooks/spec-core.mjs';
|
|
21
21
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
22
22
|
import { getLocale } from '../hooks/locale.mjs';
|
|
23
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
23
24
|
|
|
24
25
|
function today() {
|
|
25
26
|
const d = new Date();
|
|
@@ -40,7 +41,20 @@ export function runVerify(argv) {
|
|
|
40
41
|
// --project wins; otherwise climb from cwd to the nearest project marker (agent shells
|
|
41
42
|
// keep their cwd across commands, so verify from a subdirectory is a recurring miss).
|
|
42
43
|
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
43
|
-
|
|
44
|
+
let commandContext = null;
|
|
45
|
+
if (!opt(argv, '--change')) {
|
|
46
|
+
try {
|
|
47
|
+
commandContext = resolveCommandActiveContext({
|
|
48
|
+
vaultBase,
|
|
49
|
+
projectRoot,
|
|
50
|
+
sessionId: opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const slug = opt(argv, '--change') || activeChange(vaultBase, { context: commandContext });
|
|
44
58
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
45
59
|
|
|
46
60
|
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|