wendkeep 0.74.0 → 0.75.0
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 +35 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/observer.md +22 -11
- package/docs/pt-BR/commands/observer.md +22 -11
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -1
- package/schema/observer/005-project-scoped-identities.sql +217 -0
- package/src/change.mjs +41 -1
- package/src/doctor.mjs +5 -0
- package/src/init.mjs +2 -2
- package/src/note.mjs +8 -1
- package/src/observer-publish.mjs +9 -34
- package/src/observer-server.mjs +38 -63
- package/src/observer-sql-migrate.mjs +1 -1
- package/src/observer-sql-publish.mjs +372 -12
- package/src/observer-sql-store.mjs +108 -28
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +104 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ 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.0] — 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Identidades SQL escopadas por projeto.** Sessões, agentes, rollups, chamadas e transcripts
|
|
12
|
+
mantêm chaves internas derivadas de `project_id` + identificador externo, constraints compostas
|
|
13
|
+
e foreign keys que carregam o projeto. Colisões deliberadas entre projetos ficam isoladas.
|
|
14
|
+
- **Migrações comprováveis.** Cada arquivo de schema recebe checksum; uma migração estrutural de
|
|
15
|
+
base existente cria backup consistente antes da transação e pode ser retomada com segurança.
|
|
16
|
+
- **Reconciliação explícita.** `observer reconcile` reserva a varredura integral para bootstrap,
|
|
17
|
+
reparo e comprovação de paridade por hash, localmente ou contra o serviço.
|
|
18
|
+
- **Outbox observável.** `doctor` informa lotes, eventos, bytes e idade; uma lease admite apenas um
|
|
19
|
+
publisher e batches do mesmo escopo são coalescidos.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Publicação incremental nos hooks.** `SessionStart` apenas drena, `Stop` lê uma sessão e
|
|
24
|
+
`SubagentStop` somente o transcript afetado. `note new` e `change archive` enfileiram diretamente
|
|
25
|
+
os documentos que escreveram. O file store antigo deixa de receber publicação viva e permanece
|
|
26
|
+
apenas como fonte de migração.
|
|
27
|
+
- **Schema SQL 5.** O mesmo `session_id`, `agent_id`, `call_id`, `transcript_id` ou `rollup_key`
|
|
28
|
+
pode existir em projetos diferentes sem disputar uma linha global.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Ingest atômico por evento.** Cada evento usa `SAVEPOINT ingest_one`; qualquer falha reverte o
|
|
33
|
+
registro de ingestão e todas as projeções antes de o batch continuar. Retry permanece idempotente.
|
|
34
|
+
- **Chamadas incrementais estáveis.** Turnos concluídos usam identidade causal estável, evitando
|
|
35
|
+
duplicação quando um transcript cresce após a primeira publicação.
|
|
36
|
+
|
|
37
|
+
### Performance
|
|
38
|
+
|
|
39
|
+
- O caminho de hook não varre mais o Vault. A fixture de publicação incremental comprova
|
|
40
|
+
`p95 < 200 ms`, uma sessão lida por `Stop` e zero documentos lidos por `SessionStart`.
|
|
41
|
+
|
|
7
42
|
## [0.74.0] — 2026-08-20
|
|
8
43
|
|
|
9
44
|
### 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;
|
|
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, while `observer reconcile` reserves full scans for explicit recovery. |
|
|
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. `
|
|
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,
|
|
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;
|
|
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, while `observer reconcile` reserves full scans for explicit recovery. |
|
|
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. `
|
|
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,
|
|
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,8 +85,8 @@ 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`
|
|
88
|
-
|
|
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
|
|
@@ -96,16 +97,26 @@ cost/token total recorded in frontmatter is preserved through an explicit reconc
|
|
|
96
97
|
the detailed ledger does not add up; that row does not invent calls. Historical sessions sharing
|
|
97
98
|
one `session_id` receive a canonical per-file identity so one rollup cannot overwrite the other.
|
|
98
99
|
|
|
99
|
-
In schema
|
|
100
|
+
In schema 5, sessions, agents, rollups, calls, and transcripts use internal identities derived from
|
|
101
|
+
`project_id` plus the external identifier, with project-scoped constraints and foreign keys. The
|
|
102
|
+
same `session_id`, `agent_id`, `call_id`, or `rollup_key` can exist in separate projects without a
|
|
103
|
+
collision. Every event runs inside its own savepoint; an intermediate failure rolls back the ingest
|
|
104
|
+
row and all projections before the batch continues. Migrations are checksummed and an existing
|
|
105
|
+
database receives a consistent backup before every structural migration.
|
|
106
|
+
|
|
107
|
+
Each ingested document is also projected into chunks carrying path, heading,
|
|
100
108
|
authority, time, and validity. The Observer feature-probes FTS5 and uses the index when the
|
|
101
109
|
extension is available; otherwise it preserves the same semantics through a lexical fallback.
|
|
102
110
|
Search returns the passage containing the match and its provenance rather than only the beginning
|
|
103
111
|
of the document.
|
|
104
112
|
|
|
105
113
|
`init` projects `observer-publish` into `SessionStart`, `Stop`, and `SubagentStop` after the primary
|
|
106
|
-
hooks.
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
hooks. `SessionStart` only drains the queue; `Stop` reads the changed session alone, and
|
|
115
|
+
`SubagentStop` reads only the affected subagent transcript. `note new` and `change archive` enqueue
|
|
116
|
+
the documents they wrote. Events are coalesced by scope and one lease admits a single publisher.
|
|
117
|
+
When the server is unavailable, `.brain/observer-sql-outbox/` remains durable without blocking the
|
|
118
|
+
session; full-vault scanning is reserved for `observer reconcile`. `doctor` reports queue batches,
|
|
119
|
+
events, bytes, and age. SQL batches use gzip so complete transcripts larger than 64 MB as plain JSON remain within
|
|
109
120
|
the transport limit; the Observer decompresses and validates the body before ingesting it. The
|
|
110
121
|
outbox is temporary transport, not authority.
|
|
111
122
|
|
|
@@ -113,12 +124,12 @@ outbox is temporary transport, not authority.
|
|
|
113
124
|
|
|
114
125
|
- `project_not_registered`: run `observer register` before publishing.
|
|
115
126
|
- `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
|
-
|
|
118
|
-
`.brain/observer-sql-state.json
|
|
127
|
+
- Pending outbox: the service was unavailable; preserve `.brain/observer-sql-outbox/` and let a
|
|
128
|
+
later hook drain it or run `observer reconcile`. Also ignore
|
|
129
|
+
`.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
130
|
- `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: run the Observer on Node.js 22.13 or newer.
|
|
120
131
|
- If memory or usage is incomplete, check the Sync screen, preserve the outbox, and run
|
|
121
|
-
`observer
|
|
132
|
+
`observer reconcile` to rebuild the load and prove hash parity.
|
|
122
133
|
|
|
123
134
|
## Next steps
|
|
124
135
|
|
|
@@ -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,8 +86,8 @@ 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`
|
|
89
|
-
|
|
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
|
|
@@ -98,15 +99,25 @@ reconciliação quando o ledger detalhado não fecha com ele; essa linha não in
|
|
|
98
99
|
Sessões históricas com o mesmo `session_id` recebem uma identidade canônica por arquivo para
|
|
99
100
|
evitar que um rollup sobrescreva o outro.
|
|
100
101
|
|
|
101
|
-
No schema
|
|
102
|
+
No schema 5, sessões, agentes, rollups, chamadas e transcripts usam identidades internas derivadas
|
|
103
|
+
de `project_id` + identificador externo, com constraints e foreign keys escopadas. O mesmo
|
|
104
|
+
`session_id`, `agent_id`, `call_id` ou `rollup_key` pode existir em projetos diferentes sem colisão.
|
|
105
|
+
Cada evento é aplicado em um savepoint próprio; falha intermediária reverte ingest e todas as
|
|
106
|
+
projeções antes de o batch continuar. Migrações têm checksum e toda migração estrutural de uma base
|
|
107
|
+
existente cria backup consistente antes da transação.
|
|
108
|
+
|
|
109
|
+
Cada documento ingerido também é projetado em chunks com caminho, heading,
|
|
102
110
|
autoridade, data e validade. O Observer faz um feature probe de FTS5 e usa o índice quando a
|
|
103
111
|
extensão está disponível; caso contrário, mantém a mesma semântica por fallback lexical. A busca
|
|
104
112
|
retorna o trecho em que houve o match e sua proveniência, não apenas o começo do documento.
|
|
105
113
|
|
|
106
114
|
O `init` projeta `observer-publish` para `SessionStart`, `Stop` e `SubagentStop` depois dos hooks
|
|
107
|
-
principais.
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
principais. `SessionStart` apenas drena a fila; `Stop` lê somente a sessão alterada e
|
|
116
|
+
`SubagentStop` somente o transcript do subagente afetado. `note new` e `change archive` enfileiram
|
|
117
|
+
diretamente os documentos que escreveram. Os eventos são coalescidos por escopo e uma lease garante
|
|
118
|
+
um único publisher. Sem servidor disponível, a outbox `.brain/observer-sql-outbox/` é preservada
|
|
119
|
+
sem bloquear a sessão; a varredura integral fica reservada a `observer reconcile`. O `doctor`
|
|
120
|
+
mostra lotes, eventos, bytes e idade da fila. Os lotes SQL são enviados com gzip para que transcripts completos maiores que
|
|
110
121
|
64 MB em JSON puro continuem dentro do limite do transporte; o Observer descomprime e valida o
|
|
111
122
|
corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
|
|
112
123
|
|
|
@@ -114,12 +125,12 @@ corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
|
|
|
114
125
|
|
|
115
126
|
- `project_not_registered`: rode `observer register` antes de publicar.
|
|
116
127
|
- `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
|
-
|
|
119
|
-
`.brain/observer-sql-state.json
|
|
128
|
+
- Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-sql-outbox/` e deixe
|
|
129
|
+
um hook posterior drená-la ou rode `observer reconcile`. Adicione também
|
|
130
|
+
`.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
131
|
- `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: execute o Observer em Node.js 22.13 ou mais recente.
|
|
121
132
|
- Se a memória ou o consumo ficarem incompletos, verifique a tela Sincronização, preserve o
|
|
122
|
-
outbox e rode `observer
|
|
133
|
+
outbox e rode `observer reconcile` para reconstruir e comprovar paridade por hash.
|
|
123
134
|
|
|
124
135
|
## Próximos passos
|
|
125
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.0",
|
|
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);
|
package/src/init.mjs
CHANGED
|
@@ -323,7 +323,7 @@ const MESSAGES = {
|
|
|
323
323
|
mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
|
|
324
324
|
colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
|
|
325
325
|
colors: (r) => ` [5/5] cores: ${r}`,
|
|
326
|
-
runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
|
|
326
|
+
runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
|
|
327
327
|
merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
|
|
328
328
|
nextSteps: '\nPróximos passos:',
|
|
329
329
|
step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
|
|
@@ -352,7 +352,7 @@ const MESSAGES = {
|
|
|
352
352
|
mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
|
|
353
353
|
colorsSkipped: ' [5/5] colors skipped (--no-colors)',
|
|
354
354
|
colors: (r) => ` [5/5] colors: ${r}`,
|
|
355
|
-
runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
|
|
355
|
+
runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
|
|
356
356
|
merged: 'merged', created: 'created', bakSaved: ', .bak saved',
|
|
357
357
|
nextSteps: '\nNext steps:',
|
|
358
358
|
step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
|
package/src/note.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import { getLocale } from '../hooks/locale.mjs';
|
|
|
17
17
|
import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
|
|
18
18
|
import { repairStackedFrontmatter } from '../hooks/frontmatter-repair.mjs';
|
|
19
19
|
import { repairDerivedSections } from '../hooks/derived-sections.mjs';
|
|
20
|
+
import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
|
|
21
|
+
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
20
22
|
|
|
21
23
|
const TYPES = {
|
|
22
24
|
bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
|
|
@@ -120,6 +122,11 @@ export function runNote(argv) {
|
|
|
120
122
|
} catch { /* sem sessão ativa — nota nasce sem backlink */ }
|
|
121
123
|
|
|
122
124
|
writeFileSync(filePath, kind.build(title.trim(), { num, dateStr, sessionRel, localeId: loc.id }), 'utf8');
|
|
123
|
-
|
|
125
|
+
const logicalPath = toVaultRelative(vaultBase, filePath);
|
|
126
|
+
try {
|
|
127
|
+
const project = readProjectForValidation(vaultBase);
|
|
128
|
+
if (project.ok && project.projectId) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath });
|
|
129
|
+
} catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
|
|
130
|
+
process.stdout.write(`${logicalPath}\n`);
|
|
124
131
|
process.exit(0);
|
|
125
132
|
}
|