wendkeep 0.74.0 → 0.75.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ 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.75.1] — 2026-08-20
8
+
9
+ ### Fixed
10
+
11
+ - **Coalescência sem perda.** A outbox seleciona o identificador próprio de cada tipo de evento;
12
+ chamadas, agentes, rollups e transcripts distintos da mesma sessão não disputam mais a mesma chave.
13
+ - **Reconciliação realmente integral.** `observer reconcile --url` e `observer memory import`
14
+ ignoram o cursor incremental local, consultam as revisões remotas e regeneram documentos,
15
+ consumo, chamadas e transcripts mesmo após restauração ou troca do Observer.
16
+ - **Leases compatíveis com o transporte.** Locks de batch e publisher permanecem válidos além do
17
+ timeout máximo de request e só podem ser liberados pelo proprietário que os adquiriu.
18
+
19
+ ## [0.75.0] — 2026-08-20
20
+
21
+ ### Added
22
+
23
+ - **Identidades SQL escopadas por projeto.** Sessões, agentes, rollups, chamadas e transcripts
24
+ mantêm chaves internas derivadas de `project_id` + identificador externo, constraints compostas
25
+ e foreign keys que carregam o projeto. Colisões deliberadas entre projetos ficam isoladas.
26
+ - **Migrações comprováveis.** Cada arquivo de schema recebe checksum; uma migração estrutural de
27
+ base existente cria backup consistente antes da transação e pode ser retomada com segurança.
28
+ - **Reconciliação explícita.** `observer reconcile` reserva a varredura integral para bootstrap,
29
+ reparo e comprovação de paridade por hash, localmente ou contra o serviço.
30
+ - **Outbox observável.** `doctor` informa lotes, eventos, bytes e idade; uma lease admite apenas um
31
+ publisher e batches do mesmo escopo são coalescidos.
32
+
33
+ ### Changed
34
+
35
+ - **Publicação incremental nos hooks.** `SessionStart` apenas drena, `Stop` lê uma sessão e
36
+ `SubagentStop` somente o transcript afetado. `note new` e `change archive` enfileiram diretamente
37
+ os documentos que escreveram. O file store antigo deixa de receber publicação viva e permanece
38
+ apenas como fonte de migração.
39
+ - **Schema SQL 5.** O mesmo `session_id`, `agent_id`, `call_id`, `transcript_id` ou `rollup_key`
40
+ pode existir em projetos diferentes sem disputar uma linha global.
41
+
42
+ ### Fixed
43
+
44
+ - **Ingest atômico por evento.** Cada evento usa `SAVEPOINT ingest_one`; qualquer falha reverte o
45
+ registro de ingestão e todas as projeções antes de o batch continuar. Retry permanece idempotente.
46
+ - **Chamadas incrementais estáveis.** Turnos concluídos usam identidade causal estável, evitando
47
+ duplicação quando um transcript cresce após a primeira publicação.
48
+
49
+ ### Performance
50
+
51
+ - O caminho de hook não varre mais o Vault. A fixture de publicação incremental comprova
52
+ `p95 < 200 ms`, uma sessão lida por `Stop` e zero documentos lidos por `SessionStart`.
53
+
7
54
  ## [0.74.0] — 2026-08-20
8
55
 
9
56
  ### Added
package/README.en.md CHANGED
@@ -88,7 +88,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
88
88
  | **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault; research previews without a final rate remain unestimated. |
89
89
  | **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
90
90
  | **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
91
- | **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite; search returns the matching passage with provenance and falls back to the local lexical ranker when FTS5 is unavailable. |
91
+ | **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed; `observer reconcile --url` ignores the incremental cursor to regenerate the complete projection while preserving local/remote revision baselines. |
92
92
 
93
93
  During historical migration, the Observer preserves differences between frontmatter totals and the
94
94
  ledger as explicit reconciliation rows, and disambiguates duplicate `session_id` values per file
@@ -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. `observer-publish` keeps the sanitized index projection and also sends the local SQL authority with documents, consumption, and transcripts; it does not replace the local lifecycle. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. 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`.
@@ -240,7 +240,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
240
240
  | **Notes and knowledge** | BUG/APR/ADR, repairs, renumbering, lessons, and dashboard | [Notes and knowledge](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/notes-and-knowledge.md) |
241
241
  | **Costs and observability** | safe dry-run, tri-state, aggregation, trends, and historical rebuild | [Costs and observability](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/costs-and-observability.md) |
242
242
  | **Maintenance and diagnostics** | doctor, frontier/manifest freshness, drift, version, and help | [Maintenance and diagnostics](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/maintenance-and-diagnostics.md) |
243
- | **Local Observer** | `observer serve`, registration, snapshots, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
243
+ | **Local Observer** | `observer serve`, registration, incremental publication, `reconcile`, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
244
244
 
245
245
  Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/verify.md),
246
246
  [legacy-memory migration](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory-migration.md), and
package/README.md CHANGED
@@ -88,7 +88,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
88
88
  | **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault; research previews without a final rate remain unestimated. |
89
89
  | **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
90
90
  | **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
91
- | **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite; search returns the matching passage with provenance and falls back to the local lexical ranker when FTS5 is unavailable. |
91
+ | **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed; `observer reconcile --url` ignores the incremental cursor to regenerate the complete projection while preserving local/remote revision baselines. |
92
92
 
93
93
  During historical migration, the Observer preserves differences between frontmatter totals and the
94
94
  ledger as explicit reconciliation rows, and disambiguates duplicate `session_id` values per file
@@ -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. `observer-publish` keeps the sanitized index projection and also sends the local SQL authority with documents, consumption, and transcripts; it does not replace the local lifecycle. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. 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`.
@@ -240,7 +240,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
240
240
  | **Notes and knowledge** | BUG/APR/ADR, repairs, renumbering, lessons, and dashboard | [Notes and knowledge](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/notes-and-knowledge.md) |
241
241
  | **Costs and observability** | safe dry-run, tri-state, aggregation, trends, and historical rebuild | [Costs and observability](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/costs-and-observability.md) |
242
242
  | **Maintenance and diagnostics** | doctor, frontier/manifest freshness, drift, version, and help | [Maintenance and diagnostics](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/maintenance-and-diagnostics.md) |
243
- | **Local Observer** | `observer serve`, registration, snapshots, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
243
+ | **Local Observer** | `observer serve`, registration, incremental publication, `reconcile`, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
244
244
 
245
245
  Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/verify.md),
246
246
  [legacy-memory migration](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory-migration.md), and
@@ -34,6 +34,7 @@ loopback reads remain open, while every mutation requires a Bearer token.
34
34
  npx wendkeep observer status --data-dir <directory> --json
35
35
  npx wendkeep observer register --project <project> --vault <vault> --data-dir <directory>
36
36
  npx wendkeep observer publish --project <project> --vault <vault> --data-dir <directory>
37
+ npx wendkeep observer reconcile --project <project> --vault <vault> --data-dir <directory> [--url http://127.0.0.1:8787]
37
38
  npx wendkeep observer memory import --project <project> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
39
  npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory> --token <token>
39
40
  ```
@@ -42,7 +43,7 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory>
42
43
 
43
44
  - `--data-dir` selects the local event and index directory; the default is
44
45
  `WENDKEEP_OBSERVER_DATA_DIR` or `~/.wendkeep-observer`.
45
- - `--project` and `--vault` identify a project for `register`, `publish`, and `memory import`.
46
+ - `--project` and `--vault` identify a project for `register`, `publish`, `reconcile`, and `memory import`.
46
47
  - `--host` accepts only `127.0.0.1`, `localhost`, or `::1`; other hosts are rejected before
47
48
  listening.
48
49
  - `--token` or `WENDKEEP_OBSERVER_TOKEN` authenticates mutations; `--allow-non-loopback` fails without one.
@@ -84,28 +85,43 @@ If the browser shows the shell but the list fails, check the service health at
84
85
 
85
86
  ## Expected result
86
87
 
87
- `register` stores `project_id`, name, version, and registration time. `publish` reads the local
88
- vault, produces the snapshot, and sends idempotent events to SQLite containing the complete content
88
+ `register` stores `project_id`, name, version, and registration time. `publish` and `reconcile`
89
+ perform an explicit full-vault scan and send idempotent events to SQLite containing the complete content
89
90
  of sessions, decisions, bugs, learnings, specs, changes, CORE, DIGEST, SHARED_MEMORY, brain state,
90
91
  agent sessions, cost rollups, and calls. Messages and transcripts are only sent by capture levels
91
92
  that explicitly enable them. The container stores everything in
92
93
  `/data/observer.sqlite`; it does not mount `C:\GitHub` or any `.WendKeep-vault`. Markdown is only
93
94
  the text held in SQL and is recreated as files only by an explicit read-only export.
94
- `memory import` performs the initial load and returns file/hash parity. During migration, the
95
- cost/token total recorded in frontmatter is preserved through an explicit reconciliation row when
95
+ `memory import` performs the initial load and returns file/hash parity. The remote recovery
96
+ commands `observer reconcile --url` and `observer memory import` do not use the
97
+ incremental cursor to decide what to omit: they regenerate documents, sessions, usage, calls, and
98
+ transcripts. The cursor still supplies the local revision baseline and the remote tree, when
99
+ available, supplies the highest known baseline; a transient read failure never lowers the locally
100
+ persisted revision. During migration, the cost/token total recorded in frontmatter is preserved
101
+ through an explicit reconciliation row when
96
102
  the detailed ledger does not add up; that row does not invent calls. Historical sessions sharing
97
103
  one `session_id` receive a canonical per-file identity so one rollup cannot overwrite the other.
98
104
 
99
- In schema 4, each ingested document is also projected into chunks carrying path, heading,
105
+ In schema 5, sessions, agents, rollups, calls, and transcripts use internal identities derived from
106
+ `project_id` plus the external identifier, with project-scoped constraints and foreign keys. The
107
+ same `session_id`, `agent_id`, `call_id`, or `rollup_key` can exist in separate projects without a
108
+ collision. Every event runs inside its own savepoint; an intermediate failure rolls back the ingest
109
+ row and all projections before the batch continues. Migrations are checksummed and an existing
110
+ database receives a consistent backup before every structural migration.
111
+
112
+ Each ingested document is also projected into chunks carrying path, heading,
100
113
  authority, time, and validity. The Observer feature-probes FTS5 and uses the index when the
101
114
  extension is available; otherwise it preserves the same semantics through a lexical fallback.
102
115
  Search returns the passage containing the match and its provenance rather than only the beginning
103
116
  of the document.
104
117
 
105
118
  `init` projects `observer-publish` into `SessionStart`, `Stop`, and `SubagentStop` after the primary
106
- hooks. When the server is unavailable, it writes snapshots to `.brain/observer-outbox/` and SQL
107
- events to `.brain/observer-sql-outbox/` without blocking the session; a later run retries the
108
- batches. SQL batches use gzip so complete transcripts larger than 64 MB as plain JSON remain within
119
+ hooks. `SessionStart` only drains the queue; `Stop` reads the changed session alone, and
120
+ `SubagentStop` reads only the affected subagent transcript. `note new` and `change archive` enqueue
121
+ the documents they wrote. Events are coalesced by scope and one lease admits a single publisher.
122
+ When the server is unavailable, `.brain/observer-sql-outbox/` remains durable without blocking the
123
+ session; full-vault scanning is reserved for `observer reconcile`. `doctor` reports queue batches,
124
+ events, bytes, and age. SQL batches use gzip so complete transcripts larger than 64 MB as plain JSON remain within
109
125
  the transport limit; the Observer decompresses and validates the body before ingesting it. The
110
126
  outbox is temporary transport, not authority.
111
127
 
@@ -113,12 +129,12 @@ outbox is temporary transport, not authority.
113
129
 
114
130
  - `project_not_registered`: run `observer register` before publishing.
115
131
  - `host loopback`: replace `0.0.0.0` or a LAN address with `127.0.0.1`.
116
- - Pending outbox: the service was unavailable; preserve `.brain/observer-outbox/` and
117
- `.brain/observer-sql-outbox/`, then rerun the publisher. Also ignore
118
- `.brain/observer-sql-state.json` and `.brain/observer-sql-outbox/` in a versioned vault. Do not delete events manually.
132
+ - Pending outbox: the service was unavailable; preserve `.brain/observer-sql-outbox/` and let a
133
+ later hook drain it or run `observer reconcile`. Also ignore
134
+ `.brain/observer-sql-state.json`, `.brain/observer-sql-outbox/`, and `.brain/observer-sql-publisher.lock` in a versioned vault. Do not delete events manually.
119
135
  - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: run the Observer on Node.js 22.13 or newer.
120
136
  - If memory or usage is incomplete, check the Sync screen, preserve the outbox, and run
121
- `observer memory import` to rebuild the load from the vault.
137
+ `observer reconcile` to rebuild the load and prove hash parity.
122
138
 
123
139
  ## Next steps
124
140
 
@@ -34,6 +34,7 @@ continuam compatíveis com Node.js 18 ou mais recente. Registre explicitamente c
34
34
  npx wendkeep observer status --data-dir <diretório> --json
35
35
  npx wendkeep observer register --project <projeto> --vault <vault> --data-dir <diretório>
36
36
  npx wendkeep observer publish --project <projeto> --vault <vault> --data-dir <diretório>
37
+ npx wendkeep observer reconcile --project <projeto> --vault <vault> --data-dir <diretório> [--url http://127.0.0.1:8787]
37
38
  npx wendkeep observer memory import --project <projeto> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
39
  npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório> --token <token>
39
40
  ```
@@ -42,7 +43,7 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório>
42
43
 
43
44
  - `--data-dir` escolhe o diretório local de eventos e índice; o padrão é
44
45
  `WENDKEEP_OBSERVER_DATA_DIR` ou `~/.wendkeep-observer`.
45
- - `--project` e `--vault` identificam o projeto nos comandos `register`, `publish` e `memory import`.
46
+ - `--project` e `--vault` identificam o projeto nos comandos `register`, `publish`, `reconcile` e `memory import`.
46
47
  - `--host` aceita somente `127.0.0.1`, `localhost` ou `::1`; outros hosts são recusados antes do
47
48
  listen.
48
49
  - `--token` ou `WENDKEEP_OBSERVER_TOKEN` autentica mutações; `--allow-non-loopback` falha sem token.
@@ -85,28 +86,43 @@ Se o navegador mostrar a tela mas a lista falhar, confirme a saúde em
85
86
 
86
87
  ## Resultado esperado
87
88
 
88
- `register` grava `project_id`, nome, versão e data de registro. `publish` o vault local,
89
- produz o snapshot e envia eventos idempotentes para o SQLite com o conteúdo integral das sessões,
89
+ `register` grava `project_id`, nome, versão e data de registro. `publish` e `reconcile` fazem a
90
+ varredura integral explícita do vault e enviam eventos idempotentes para o SQLite com o conteúdo integral das sessões,
90
91
  decisões, bugs, aprendizados, specs, changes, CORE, DIGEST, SHARED_MEMORY, estado do brain,
91
92
  sessões de agentes, rollups de custo e chamadas. Mensagens e transcripts só são enviados nos
92
93
  níveis de captura que os habilitam. O container grava tudo em
93
94
  `/data/observer.sqlite`; não monta `C:\GitHub` nem qualquer `.WendKeep-vault`. Markdown é aceito
94
95
  somente como conteúdo de uma coluna SQL e volta a existir como arquivo apenas pela exportação
95
96
  read-only sob demanda. `memory import` faz a carga inicial e retorna a paridade por arquivo e hash.
97
+ Na recuperação remota, `observer reconcile --url` e `observer memory import` não usam o cursor
98
+ incremental para decidir o que omitir: regeneram documentos, sessões, consumo, chamadas e
99
+ transcripts. O cursor continua fornecendo o baseline local de revisão e a árvore remota, quando
100
+ disponível, fornece o maior baseline conhecido; uma falha temporária nessa leitura nunca reduz a
101
+ revisão persistida localmente.
96
102
  Na migração, o total de custo/token registrado no frontmatter é preservado por uma linha de
97
103
  reconciliação quando o ledger detalhado não fecha com ele; essa linha não inventa chamadas.
98
104
  Sessões históricas com o mesmo `session_id` recebem uma identidade canônica por arquivo para
99
105
  evitar que um rollup sobrescreva o outro.
100
106
 
101
- No schema 4, cada documento ingerido também é projetado em chunks com caminho, heading,
107
+ No schema 5, sessões, agentes, rollups, chamadas e transcripts usam identidades internas derivadas
108
+ de `project_id` + identificador externo, com constraints e foreign keys escopadas. O mesmo
109
+ `session_id`, `agent_id`, `call_id` ou `rollup_key` pode existir em projetos diferentes sem colisão.
110
+ Cada evento é aplicado em um savepoint próprio; falha intermediária reverte ingest e todas as
111
+ projeções antes de o batch continuar. Migrações têm checksum e toda migração estrutural de uma base
112
+ existente cria backup consistente antes da transação.
113
+
114
+ Cada documento ingerido também é projetado em chunks com caminho, heading,
102
115
  autoridade, data e validade. O Observer faz um feature probe de FTS5 e usa o índice quando a
103
116
  extensão está disponível; caso contrário, mantém a mesma semântica por fallback lexical. A busca
104
117
  retorna o trecho em que houve o match e sua proveniência, não apenas o começo do documento.
105
118
 
106
119
  O `init` projeta `observer-publish` para `SessionStart`, `Stop` e `SubagentStop` depois dos hooks
107
- principais. Sem servidor disponível, ele grava snapshots em `.brain/observer-outbox/` e eventos
108
- SQL em `.brain/observer-sql-outbox/`, sem bloquear a sessão; uma execução posterior tenta
109
- reenviar os lotes. Os lotes SQL são enviados com gzip para que transcripts completos maiores que
120
+ principais. `SessionStart` apenas drena a fila; `Stop` somente a sessão alterada e
121
+ `SubagentStop` somente o transcript do subagente afetado. `note new` e `change archive` enfileiram
122
+ diretamente os documentos que escreveram. Os eventos são coalescidos por escopo e uma lease garante
123
+ um único publisher. Sem servidor disponível, a outbox `.brain/observer-sql-outbox/` é preservada
124
+ sem bloquear a sessão; a varredura integral fica reservada a `observer reconcile`. O `doctor`
125
+ mostra lotes, eventos, bytes e idade da fila. Os lotes SQL são enviados com gzip para que transcripts completos maiores que
110
126
  64 MB em JSON puro continuem dentro do limite do transporte; o Observer descomprime e valida o
111
127
  corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
112
128
 
@@ -114,12 +130,12 @@ corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
114
130
 
115
131
  - `project_not_registered`: rode `observer register` antes de publicar.
116
132
  - `host loopback`: troque `0.0.0.0` ou endereço LAN por `127.0.0.1`.
117
- - Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-outbox/` e
118
- `.brain/observer-sql-outbox/` e repita o publisher. Adicione também
119
- `.brain/observer-sql-state.json` e `.brain/observer-sql-outbox/` ao ignore do Vault. Não apague eventos manualmente.
133
+ - Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-sql-outbox/` e deixe
134
+ um hook posterior drená-la ou rode `observer reconcile`. Adicione também
135
+ `.brain/observer-sql-state.json`, `.brain/observer-sql-outbox/` e `.brain/observer-sql-publisher.lock` ao ignore do Vault. Não apague eventos manualmente.
120
136
  - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: execute o Observer em Node.js 22.13 ou mais recente.
121
137
  - Se a memória ou o consumo ficarem incompletos, verifique a tela Sincronização, preserve o
122
- outbox e rode `observer memory import` para reconstruir a carga a partir do vault.
138
+ outbox e rode `observer reconcile` para reconstruir e comprovar paridade por hash.
123
139
 
124
140
  ## Próximos passos
125
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.74.0",
3
+ "version": "0.75.1",
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": [
@@ -49,7 +49,7 @@ Usage:
49
49
  cannot replace itself. · --vault P · --profile <name> · --yes.
50
50
 
51
51
  wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
52
- wendkeep observer <sub> Local multi-project Observer: serve | register | publish | status.
52
+ wendkeep observer <sub> Local multi-project Observer: serve | register | publish | reconcile | status.
53
53
  wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
54
54
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
55
55
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
@@ -0,0 +1,217 @@
1
+ -- wendkeep:structural
2
+ -- Rebuild the operational identity tables so external identifiers are scoped by project.
3
+ -- Internal primary keys remain deterministic and opaque to the public API.
4
+
5
+ PRAGMA defer_foreign_keys = ON;
6
+
7
+ CREATE TABLE sessions_v5 (
8
+ session_pk TEXT PRIMARY KEY,
9
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
10
+ session_id TEXT NOT NULL,
11
+ provider TEXT NOT NULL DEFAULT '',
12
+ status TEXT NOT NULL DEFAULT 'unknown',
13
+ summary TEXT NOT NULL DEFAULT '',
14
+ change_slug TEXT NOT NULL DEFAULT '',
15
+ started_at TEXT,
16
+ ended_at TEXT,
17
+ updated_at TEXT NOT NULL,
18
+ metadata_json TEXT NOT NULL DEFAULT '{}',
19
+ UNIQUE(project_id, session_id)
20
+ );
21
+
22
+ INSERT INTO sessions_v5
23
+ SELECT project_id || char(31) || session_id, project_id, session_id, provider, status,
24
+ summary, change_slug, started_at, ended_at, updated_at, metadata_json
25
+ FROM sessions;
26
+
27
+ INSERT OR IGNORE INTO sessions_v5(session_pk, project_id, session_id, updated_at)
28
+ SELECT project_id || char(31) || session_id, project_id, session_id,
29
+ COALESCE(MAX(occurred_at), CURRENT_TIMESTAMP)
30
+ FROM (
31
+ SELECT project_id, session_id, occurred_at FROM usage_rollups
32
+ UNION ALL SELECT project_id, session_id, occurred_at FROM llm_calls
33
+ UNION ALL SELECT project_id, session_id, occurred_at FROM transcripts
34
+ ) GROUP BY project_id, session_id;
35
+
36
+ INSERT OR IGNORE INTO sessions_v5(session_pk, project_id, session_id, updated_at)
37
+ SELECT project_id || char(31) || session_id, project_id, session_id, CURRENT_TIMESTAMP
38
+ FROM agent_runs;
39
+
40
+ CREATE TABLE agent_runs_v5 (
41
+ agent_pk TEXT PRIMARY KEY,
42
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
43
+ agent_id TEXT NOT NULL,
44
+ session_id TEXT NOT NULL,
45
+ parent_agent_id TEXT,
46
+ role TEXT NOT NULL DEFAULT 'main',
47
+ agent_name TEXT NOT NULL DEFAULT '',
48
+ agent_type TEXT NOT NULL DEFAULT '',
49
+ workflow TEXT NOT NULL DEFAULT '',
50
+ status TEXT NOT NULL DEFAULT 'unknown',
51
+ model TEXT NOT NULL DEFAULT '',
52
+ effort TEXT NOT NULL DEFAULT '',
53
+ started_at TEXT,
54
+ ended_at TEXT,
55
+ metadata_json TEXT NOT NULL DEFAULT '{}',
56
+ UNIQUE(project_id, agent_id),
57
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
58
+ FOREIGN KEY(project_id, parent_agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE SET NULL
59
+ );
60
+
61
+ INSERT INTO agent_runs_v5
62
+ SELECT a.project_id || char(31) || a.agent_id, a.project_id, a.agent_id, a.session_id,
63
+ CASE WHEN EXISTS (
64
+ SELECT 1 FROM agent_runs parent
65
+ WHERE parent.project_id = a.project_id AND parent.agent_id = a.parent_agent_id
66
+ ) THEN a.parent_agent_id ELSE NULL END,
67
+ a.role, a.agent_name, a.agent_type, a.workflow, a.status, a.model, a.effort,
68
+ a.started_at, a.ended_at, a.metadata_json
69
+ FROM agent_runs a;
70
+
71
+ INSERT OR IGNORE INTO agent_runs_v5(
72
+ agent_pk, project_id, agent_id, session_id, role, status
73
+ )
74
+ SELECT project_id || char(31) || agent_id, project_id, agent_id, session_id, role, 'unknown'
75
+ FROM (
76
+ SELECT project_id, agent_id, session_id, role FROM usage_rollups
77
+ UNION ALL SELECT project_id, agent_id, session_id, role FROM llm_calls
78
+ UNION ALL SELECT project_id, agent_id, session_id, 'main' AS role FROM transcripts
79
+ );
80
+
81
+ CREATE TABLE usage_rollups_v5 (
82
+ rollup_pk TEXT PRIMARY KEY,
83
+ rollup_key TEXT NOT NULL,
84
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
85
+ session_id TEXT NOT NULL,
86
+ agent_id TEXT NOT NULL,
87
+ role TEXT NOT NULL DEFAULT 'main',
88
+ provider TEXT NOT NULL DEFAULT '',
89
+ model_provider TEXT NOT NULL DEFAULT '',
90
+ model TEXT NOT NULL DEFAULT '',
91
+ effort TEXT NOT NULL DEFAULT '',
92
+ calls INTEGER NOT NULL DEFAULT 0,
93
+ tokens_input INTEGER NOT NULL DEFAULT 0,
94
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
95
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
96
+ tokens_output INTEGER NOT NULL DEFAULT 0,
97
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
98
+ tokens_total INTEGER NOT NULL DEFAULT 0,
99
+ cost_usd REAL NOT NULL DEFAULT 0,
100
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
101
+ pricing_source TEXT NOT NULL DEFAULT '',
102
+ pricing_version TEXT NOT NULL DEFAULT '',
103
+ wasted_usd REAL NOT NULL DEFAULT 0,
104
+ revision INTEGER NOT NULL DEFAULT 1,
105
+ occurred_at TEXT NOT NULL,
106
+ source_event_id TEXT NOT NULL REFERENCES ingest_events(event_id) ON DELETE CASCADE,
107
+ metadata_json TEXT NOT NULL DEFAULT '{}',
108
+ UNIQUE(project_id, rollup_key),
109
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
110
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
111
+ );
112
+
113
+ INSERT INTO usage_rollups_v5
114
+ SELECT project_id || char(31) || rollup_key, rollup_key, project_id, session_id,
115
+ agent_id, role, provider, model_provider, model, effort, calls, tokens_input,
116
+ tokens_cache_write, tokens_cache_read, tokens_output, tokens_reasoning,
117
+ tokens_total, cost_usd, cost_status, pricing_source, pricing_version,
118
+ wasted_usd, revision, occurred_at, source_event_id, metadata_json
119
+ FROM usage_rollups;
120
+
121
+ CREATE TABLE llm_calls_v5 (
122
+ call_pk TEXT PRIMARY KEY,
123
+ call_id TEXT NOT NULL,
124
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
125
+ session_id TEXT NOT NULL,
126
+ agent_id TEXT NOT NULL,
127
+ role TEXT NOT NULL DEFAULT 'main',
128
+ provider TEXT NOT NULL DEFAULT '',
129
+ model_provider TEXT NOT NULL DEFAULT '',
130
+ model TEXT NOT NULL DEFAULT '',
131
+ effort TEXT NOT NULL DEFAULT '',
132
+ sequence INTEGER NOT NULL DEFAULT 0,
133
+ occurred_at TEXT NOT NULL,
134
+ tokens_input INTEGER NOT NULL DEFAULT 0,
135
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
136
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
137
+ tokens_output INTEGER NOT NULL DEFAULT 0,
138
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
139
+ tokens_total INTEGER NOT NULL DEFAULT 0,
140
+ cost_usd REAL NOT NULL DEFAULT 0,
141
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
142
+ transcript_id TEXT,
143
+ prompt_text TEXT NOT NULL DEFAULT '',
144
+ response_text TEXT NOT NULL DEFAULT '',
145
+ status TEXT NOT NULL DEFAULT 'complete',
146
+ metadata_json TEXT NOT NULL DEFAULT '{}',
147
+ UNIQUE(project_id, call_id),
148
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
149
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
150
+ );
151
+
152
+ INSERT INTO llm_calls_v5
153
+ SELECT project_id || char(31) || call_id, call_id, project_id, session_id, agent_id,
154
+ role, provider, model_provider, model, effort, sequence, occurred_at,
155
+ tokens_input, tokens_cache_write, tokens_cache_read, tokens_output,
156
+ tokens_reasoning, tokens_total, cost_usd, cost_status, transcript_id,
157
+ prompt_text, response_text, status, metadata_json
158
+ FROM llm_calls;
159
+
160
+ CREATE TABLE transcripts_v5 (
161
+ transcript_pk TEXT PRIMARY KEY,
162
+ transcript_id TEXT NOT NULL,
163
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
164
+ session_id TEXT NOT NULL,
165
+ agent_id TEXT NOT NULL,
166
+ coverage TEXT NOT NULL DEFAULT 'summary_only',
167
+ codec TEXT NOT NULL DEFAULT 'gzip',
168
+ content_gzip BLOB NOT NULL,
169
+ content_sha256 TEXT NOT NULL,
170
+ original_bytes INTEGER NOT NULL,
171
+ compressed_bytes INTEGER NOT NULL,
172
+ source TEXT NOT NULL DEFAULT '',
173
+ occurred_at TEXT NOT NULL,
174
+ metadata_json TEXT NOT NULL DEFAULT '{}',
175
+ UNIQUE(project_id, transcript_id),
176
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
177
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
178
+ );
179
+
180
+ INSERT INTO transcripts_v5
181
+ SELECT project_id || char(31) || transcript_id, transcript_id, project_id, session_id,
182
+ agent_id, coverage, codec, content_gzip, content_sha256, original_bytes,
183
+ compressed_bytes, source, occurred_at, metadata_json
184
+ FROM transcripts;
185
+
186
+ DROP TABLE llm_calls;
187
+ DROP TABLE transcripts;
188
+ DROP TABLE usage_rollups;
189
+ DROP TABLE agent_runs;
190
+ DROP TABLE sessions;
191
+
192
+ ALTER TABLE sessions_v5 RENAME TO sessions;
193
+ ALTER TABLE agent_runs_v5 RENAME TO agent_runs;
194
+ ALTER TABLE usage_rollups_v5 RENAME TO usage_rollups;
195
+ ALTER TABLE llm_calls_v5 RENAME TO llm_calls;
196
+ ALTER TABLE transcripts_v5 RENAME TO transcripts;
197
+
198
+ CREATE INDEX idx_sessions_project_time ON sessions(project_id, started_at, ended_at);
199
+ CREATE INDEX idx_agent_runs_project_session ON agent_runs(project_id, session_id, role);
200
+ CREATE INDEX idx_usage_rollups_project_time ON usage_rollups(project_id, occurred_at);
201
+ CREATE INDEX idx_usage_rollups_project_agent ON usage_rollups(project_id, agent_id, role);
202
+ CREATE INDEX idx_usage_rollups_project_model ON usage_rollups(project_id, model_provider, model);
203
+ CREATE INDEX idx_llm_calls_project_time ON llm_calls(project_id, occurred_at);
204
+ CREATE INDEX idx_llm_calls_project_agent ON llm_calls(project_id, agent_id, sequence);
205
+ CREATE INDEX idx_llm_calls_project_model ON llm_calls(project_id, model_provider, model);
206
+ CREATE INDEX idx_transcripts_project_session ON transcripts(project_id, session_id, occurred_at);
207
+ CREATE INDEX idx_transcripts_project_coverage ON transcripts(project_id, coverage);
208
+
209
+ CREATE TABLE project_snapshots (
210
+ project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
211
+ event_id TEXT NOT NULL,
212
+ captured_at TEXT NOT NULL,
213
+ snapshot_json TEXT NOT NULL,
214
+ UNIQUE(project_id, event_id)
215
+ );
216
+
217
+ CREATE INDEX idx_project_snapshots_captured ON project_snapshots(captured_at);
package/src/change.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // `wendkeep change <sub>` — native change lifecycle CLI (Pilar B).
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import {
5
5
  newChange,
@@ -22,6 +22,22 @@ import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
22
22
  import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
23
23
  import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
24
24
  import { getLocale } from '../hooks/locale.mjs';
25
+ import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
26
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
27
+
28
+ function observerMarkdownUnder(vaultBase, relativeRoot) {
29
+ const output = [];
30
+ const walk = (absolute, relative) => {
31
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
32
+ const nextAbsolute = join(absolute, entry.name);
33
+ const nextRelative = join(relative, entry.name);
34
+ if (entry.isDirectory()) walk(nextAbsolute, nextRelative);
35
+ else if (entry.isFile() && entry.name.endsWith('.md')) output.push(nextRelative);
36
+ }
37
+ };
38
+ try { walk(join(vaultBase, relativeRoot), relativeRoot); } catch { /* reconcile recupera */ }
39
+ return output;
40
+ }
25
41
 
26
42
  function resolveVault(argv) {
27
43
  let vault;
@@ -299,6 +315,30 @@ export function runChange(argv) {
299
315
  process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
300
316
  process.exit(1);
301
317
  }
318
+ try {
319
+ const project = readProjectForValidation(vaultBase);
320
+ if (project.ok && project.projectId) {
321
+ const loc = getLocale(vaultBase);
322
+ for (const archivedPath of observerMarkdownUnder(vaultBase, r.archivedRel)) {
323
+ enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath: archivedPath });
324
+ const suffix = archivedPath.slice(String(r.archivedRel).length).replace(/^[\\/]+/, '');
325
+ enqueueObserverDocumentChange({
326
+ vaultBase,
327
+ projectId: project.projectId,
328
+ logicalPath: join(loc.folders.changes, slug, suffix),
329
+ deleted: true,
330
+ });
331
+ }
332
+ if (r.adrRel) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath: r.adrRel });
333
+ for (const capability of r.promoted || []) {
334
+ enqueueObserverDocumentChange({
335
+ vaultBase,
336
+ projectId: project.projectId,
337
+ logicalPath: join(loc.folders.specs, capability, 'spec.md'),
338
+ });
339
+ }
340
+ }
341
+ } catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
302
342
  process.stdout.write(`archived: ${r.archivedRel}${r.adrRel ? `; ADR: ${r.adrRel}` : '; GUIDE compacta: sem ADR'}\n`);
303
343
  if (r.promoted && r.promoted.length) process.stdout.write(`specs promovidas: ${r.promoted.join(', ')}\n`);
304
344
  if (r.specWarnings && r.specWarnings.length) for (const w of r.specWarnings) process.stderr.write(` aviso spec: ${w}\n`);
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontm
5
5
  import { runVaultHealth } from '../hooks/vault-health.mjs';
6
6
  import { checkSyncDefs } from './sync-defs.mjs';
7
7
  import { resolveProjectVault } from './project-vault.mjs';
8
+ import { inspectObserverSqlOutbox } from './observer-sql-publish.mjs';
8
9
 
9
10
  const healthStatusLabel = (status) => ({
10
11
  healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
@@ -155,6 +156,10 @@ export function runDoctor(argv) {
155
156
  // 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
156
157
  const observability = checkSessionObservability(vaultBase);
157
158
  process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
159
+ const observerOutbox = inspectObserverSqlOutbox(vaultBase);
160
+ const oldestSeconds = Math.round(observerOutbox.oldest_age_ms / 1000);
161
+ process.stdout.write(`[observer] outbox SQL: ${observerOutbox.batches} lote(s) · ${observerOutbox.events} evento(s) · ${observerOutbox.bytes} bytes${observerOutbox.batches ? ` · mais antigo: ${oldestSeconds}s` : ''}\n`);
162
+ if (observerOutbox.batches) process.stdout.write(' → wendkeep observer reconcile --project . (ou inicie o servidor para o hook drenar a fila)\n');
158
163
 
159
164
  // 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
160
165
  const act = checkSessionActivity(vaultBase);