wendkeep 0.38.3 → 0.40.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 +60 -0
- package/README.md +21 -4
- package/README.pt-BR.md +20 -4
- package/bin/wendkeep.mjs +31 -0
- package/hooks/change-core.mjs +7 -7
- package/hooks/harness-doctor.mjs +1 -1
- package/hooks/obsidian-common.mjs +14 -21
- package/hooks/sensors-core.mjs +28 -6
- package/hooks/spec-core.mjs +17 -3
- package/package.json +3 -3
- package/src/change.mjs +5 -5
- package/src/doctor.mjs +18 -8
- package/src/import.mjs +15 -0
- package/src/init.mjs +18 -8
- package/src/project-vault.mjs +221 -0
- package/src/skills-seed.mjs +9 -3
- package/src/spec.mjs +1 -1
- package/src/vault-readme.mjs +2 -2
- package/src/verify.mjs +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,66 @@ 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.40.0] — 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `parseTasks` captura **todos** os `[req:]` de uma tarefa em `reqs: string[]` (`req` permanece
|
|
12
|
+
como alias do primeiro, retrocompatível). Antes, só o primeiro entrava no pacote de
|
|
13
|
+
verificação e os demais sumiam sem aviso.
|
|
14
|
+
- Heading de requisito aceita ID puro (`### Requisito: GATE-1`) como identidade, além do
|
|
15
|
+
formato preferido `### Requisito: <ID> — <nome>`. Diagnóstico de requisito órfão agora
|
|
16
|
+
ensina o formato esperado com exemplo concreto.
|
|
17
|
+
- `findProjectRoot`: `wendkeep verify` executado de um subdiretório sobe a árvore até achar
|
|
18
|
+
`wendkeep.sensors.json`/`.wendkeep.json` (à la `.git`); `--project` continua autoritativo.
|
|
19
|
+
- `--help`/`-h` universal: qualquer subcomando com `--help` imprime a ajuda e sai com 0,
|
|
20
|
+
interceptado antes da resolução de vault — nunca executa o comando.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Regex de ID de requisito unificada entre tarefa e spec (`REQ_ID_RE_SRC`): IDs
|
|
25
|
+
multi-segmento (`API-AUTH-2`) agora são reconhecidos também nas tarefas.
|
|
26
|
+
- `wendkeep verify` distingue `wendkeep.sensors.json` ausente (aviso com path + dica
|
|
27
|
+
`--project`) de JSON inválido (erro alto com a mensagem do parse). Antes, ambos viravam
|
|
28
|
+
"sensor não definido" para todos os sensores.
|
|
29
|
+
- `wendkeep import` com flag desconhecida agora falha com exit 2 citando a flag, em vez de
|
|
30
|
+
cair no default destrutivo `--source all` (que chegou a importar 78 sessões sem querer).
|
|
31
|
+
- Templates seed (skills de workflow pt/en) documentam o formato de heading de requisito e o
|
|
32
|
+
suporte a múltiplos `[req:]` por tarefa.
|
|
33
|
+
|
|
34
|
+
## [0.39.0] — 2026-07-13
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Configuração provider-neutral `.wendkeep.json`, descoberta do diretório da sessão para os
|
|
39
|
+
pais, permitindo que Codex e Claude Code resolvam o mesmo vault sem variável global do
|
|
40
|
+
Windows. Caminhos relativos ao projeto e caminhos absolutos são suportados.
|
|
41
|
+
- Identidade estável `projectId`, espelhada em `.brain/PROJECT.json`; vínculos que apontem
|
|
42
|
+
para o vault de outro projeto são rejeitados antes de qualquer gravação.
|
|
43
|
+
- `doctor` informa caminho e origem do vínculo efetivo. Comandos CLI executados dentro do
|
|
44
|
+
projeto também descobrem o vault local, mantendo `--vault` como override explícito.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- `wendkeep init` cria o vínculo projeto→vault de forma idempotente e adota instalações
|
|
49
|
+
antigas registradas em `.claude/settings.json`, sem mover ou dividir o histórico.
|
|
50
|
+
- Um vínculo local sempre vence um `OBSIDIAN_VAULT_PATH` herdado pelo processo. A variável
|
|
51
|
+
permanece apenas para compatibilidade de comandos manuais legados.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Hooks do Codex deixam de gravar sessões no vault doméstico `~/wendkeep-vault` quando o
|
|
56
|
+
processo não recebe o ambiente privado do Claude Code. Sem vínculo local ou payload
|
|
57
|
+
explícito, o hook agora falha de modo seguro, emite diagnóstico e não cria arquivos.
|
|
58
|
+
- Projetos simultâneos deixam de compartilhar acidentalmente sessões, mudanças e grafo por
|
|
59
|
+
causa de uma variável global de usuário apontando para um único vault.
|
|
60
|
+
|
|
61
|
+
### Migration
|
|
62
|
+
|
|
63
|
+
- Após atualizar, execute uma vez `wendkeep init --project . --vault <vault> --yes`, depois
|
|
64
|
+
`wendkeep sync-defs --project . --reseed` e `wendkeep doctor --project .`; reinicie Codex e
|
|
65
|
+
Claude Code para recarregar os artefatos gerados.
|
|
66
|
+
|
|
7
67
|
## [0.38.3] — 2026-07-12
|
|
8
68
|
|
|
9
69
|
### Fixed
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ npx wendkeep init
|
|
|
60
60
|
`wendkeep init` is interactive and **idempotent**. It will:
|
|
61
61
|
|
|
62
62
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
63
|
-
2.
|
|
63
|
+
2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault. Codex and Claude Code discover the same vault from their session `cwd`; no machine-global environment variable is required. Existing `.claude/settings.json` registrations are adopted automatically.
|
|
64
64
|
3. 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`.)
|
|
65
65
|
4. Offer to pin **companion** plugins/MCP (multi-choice; only `context-mode` pre-checked). Each is wired the most agent-agnostic way it supports:
|
|
66
66
|
- **`context-mode`** — context optimizer + FTS5 memory, as a `.mcp.json` MCP server (any agent). The recommended default.
|
|
@@ -96,15 +96,32 @@ npx wendkeep init --no-companions --no-mcp --yes # zero companions,
|
|
|
96
96
|
|
|
97
97
|
Then open the vault in Obsidian, send a test prompt in your agent, and confirm a note appears under `02-Sessões/…` (or `02-Sessions/…` for an `en` vault).
|
|
98
98
|
|
|
99
|
+
### Project isolation
|
|
100
|
+
|
|
101
|
+
Each project owns a `.wendkeep.json` containing a stable `projectId` and its vault path.
|
|
102
|
+
Relative paths (for example `.NutriGymBrain`) are resolved from the project root; absolute
|
|
103
|
+
paths are also supported. Hooks search upward from the agent's `cwd`, so nested packages use
|
|
104
|
+
the nearest binding. The vault carries the same identity in `.brain/PROJECT.json`; a mismatch
|
|
105
|
+
is rejected before any session is written. If no binding exists, hooks fail closed and never
|
|
106
|
+
create the historical `~/wendkeep-vault` fallback.
|
|
107
|
+
|
|
108
|
+
`OBSIDIAN_VAULT_PATH` remains only as legacy/manual CLI compatibility. It is not used to
|
|
109
|
+
route automatic Codex or Claude hooks and a project-local binding overrides an inherited
|
|
110
|
+
machine value.
|
|
111
|
+
|
|
99
112
|
## Updating
|
|
100
113
|
|
|
101
|
-
Because the hooks live inside the installed package
|
|
114
|
+
Because the hooks live inside the installed package, upgrade the package and re-run the
|
|
115
|
+
idempotent init. The init step creates/migrates the provider-neutral project binding:
|
|
102
116
|
|
|
103
117
|
```bash
|
|
104
|
-
npm
|
|
118
|
+
npm install --save-dev wendkeep@latest
|
|
119
|
+
npx --no-install wendkeep init --project . --vault <your-vault> --yes
|
|
120
|
+
npx --no-install wendkeep sync-defs --project . --reseed
|
|
121
|
+
npx --no-install wendkeep doctor --project .
|
|
105
122
|
```
|
|
106
123
|
|
|
107
|
-
|
|
124
|
+
Restart Codex and Claude Code after reseeding their generated skills.
|
|
108
125
|
|
|
109
126
|
## Commands
|
|
110
127
|
|
package/README.pt-BR.md
CHANGED
|
@@ -58,7 +58,7 @@ npx wendkeep init
|
|
|
58
58
|
O `wendkeep init` é interativo e **idempotente**. Ele:
|
|
59
59
|
|
|
60
60
|
1. Cria a taxonomia de pastas do cofre e um `README.md` templado (cofre padrão: `<projeto>/.<nome-do-projeto>-vault`, ex.: `.MeuApp-vault`; sobrescreva com `--vault`).
|
|
61
|
-
2.
|
|
61
|
+
2. Grava um vínculo provider-neutral **`.wendkeep.json`** na raiz do projeto e o marcador correspondente `.brain/PROJECT.json` no cofre. Codex e Claude Code descobrem o mesmo vault pelo `cwd` da sessão, sem variável global do Windows. Registros antigos em `.claude/settings.json` são adotados automaticamente.
|
|
62
62
|
3. Adiciona o servidor MCP **`wendkeep-vault`** ao `.mcp.json` pro agente ler/escrever o cofre. Pule com `--no-mcp` — ex.: quando o agente já tem um MCP de cofre. (`--no-mcp` pula *só o MCP do próprio wendkeep*; os MCPs de companion seguem `--companions`.)
|
|
63
63
|
4. Oferece fixar plugins/MCP **companion** (múltipla escolha; **nenhum** pré-marcado). Cada um é wirado do jeito mais agnóstico que suporta:
|
|
64
64
|
- **`context-mode`** — otimizador de contexto + memória FTS5, como servidor MCP no `.mcp.json` (qualquer agente).
|
|
@@ -94,15 +94,31 @@ npx wendkeep init --no-companions --no-mcp --yes # zero companions,
|
|
|
94
94
|
|
|
95
95
|
Depois abra o cofre no Obsidian, mande um prompt de teste no seu agente e confirme que uma nota aparece em `02-Sessões/…` (ou `02-Sessions/…` num cofre `en`).
|
|
96
96
|
|
|
97
|
+
### Isolamento por projeto
|
|
98
|
+
|
|
99
|
+
Cada projeto possui um `.wendkeep.json` com `projectId` estável e caminho do vault. Caminhos
|
|
100
|
+
relativos, como `.NutriGymBrain`, partem da raiz do projeto; caminhos absolutos também são
|
|
101
|
+
aceitos. Os hooks procuram o vínculo mais próximo subindo a partir do `cwd`. O vault guarda a
|
|
102
|
+
mesma identidade em `.brain/PROJECT.json`, e uma divergência bloqueia a escrita. Sem vínculo,
|
|
103
|
+
os hooks falham de modo seguro e nunca criam o antigo fallback `~/wendkeep-vault`.
|
|
104
|
+
|
|
105
|
+
`OBSIDIAN_VAULT_PATH` permanece somente como compatibilidade legada para comandos manuais.
|
|
106
|
+
Ele não roteia hooks automáticos do Codex ou Claude, e o vínculo local prevalece sobre uma
|
|
107
|
+
variável herdada da máquina.
|
|
108
|
+
|
|
97
109
|
## Atualizar
|
|
98
110
|
|
|
99
|
-
Como os hooks vivem dentro do pacote instalado
|
|
111
|
+
Como os hooks vivem dentro do pacote instalado, atualize e rode novamente o `init`
|
|
112
|
+
idempotente. Essa etapa cria ou migra o vínculo provider-neutral:
|
|
100
113
|
|
|
101
114
|
```bash
|
|
102
|
-
npm
|
|
115
|
+
npm install --save-dev wendkeep@latest
|
|
116
|
+
npx --no-install wendkeep init --project . --vault <seu-vault> --yes
|
|
117
|
+
npx --no-install wendkeep sync-defs --project . --reseed
|
|
118
|
+
npx --no-install wendkeep doctor --project .
|
|
103
119
|
```
|
|
104
120
|
|
|
105
|
-
|
|
121
|
+
Reinicie Codex e Claude Code depois de resemear as skills geradas.
|
|
106
122
|
|
|
107
123
|
## Comandos
|
|
108
124
|
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -102,8 +102,39 @@ function runHook(name) {
|
|
|
102
102
|
process.exit(r.status ?? 0);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
function optionValue(argv, name) {
|
|
106
|
+
const index = argv.indexOf(name);
|
|
107
|
+
if (index >= 0) return argv[index + 1] || '';
|
|
108
|
+
return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function preferProjectVault(argv) {
|
|
112
|
+
// Existing command modules still consume OBSIDIAN_VAULT_PATH internally. Populate it
|
|
113
|
+
// only inside this CLI process from the provider-neutral project binding, overriding
|
|
114
|
+
// any inherited machine-global value. An explicit --vault remains authoritative.
|
|
115
|
+
if (optionValue(argv, '--vault')) return;
|
|
116
|
+
try {
|
|
117
|
+
const { resolveProjectVault } = await import('../src/project-vault.mjs');
|
|
118
|
+
const resolved = resolveProjectVault({ startDir: optionValue(argv, '--project') || process.cwd() });
|
|
119
|
+
process.env.OBSIDIAN_VAULT_PATH = resolved.base;
|
|
120
|
+
} catch {
|
|
121
|
+
// Backward-compatible manual CLI behavior: individual commands still explain
|
|
122
|
+
// --vault / legacy env when no project binding exists. Hooks do not use this path.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
async function main() {
|
|
106
127
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
128
|
+
// Universal --help: any subcommand with --help/-h prints usage and never executes.
|
|
129
|
+
// Intercepted BEFORE vault resolution so it works anywhere — help must never depend
|
|
130
|
+
// on project state, and no command may treat --help as a runnable default.
|
|
131
|
+
if (cmd && (rest.includes('--help') || rest.includes('-h'))) {
|
|
132
|
+
process.stdout.write(HELP);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
if (cmd && !['init', 'hook', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
136
|
+
await preferProjectVault(rest);
|
|
137
|
+
}
|
|
107
138
|
switch (cmd) {
|
|
108
139
|
case 'init': {
|
|
109
140
|
const { runInit } = await import('../src/init.mjs');
|
package/hooks/change-core.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
|
-
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline } from './spec-core.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline, REQ_ID_RE_SRC } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
10
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
@@ -161,18 +161,18 @@ export function parseTasks(md) {
|
|
|
161
161
|
const tasks = [];
|
|
162
162
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
163
163
|
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
164
|
-
const
|
|
164
|
+
const reqReG = new RegExp(`\\[req:\\s*(${REQ_ID_RE_SRC})\\]`, 'g');
|
|
165
165
|
let m;
|
|
166
166
|
while ((m = re.exec(String(md))) !== null) {
|
|
167
167
|
let text = m[3].trim();
|
|
168
168
|
const sm = text.match(sensorRe);
|
|
169
|
-
const
|
|
169
|
+
const reqs = [...text.matchAll(reqReG)].map((r) => r[1]);
|
|
170
170
|
const sensor = sm ? sm[1] : undefined;
|
|
171
|
-
const req = rm ? rm[1] : undefined;
|
|
172
171
|
if (sm) text = text.replace(sensorRe, '');
|
|
173
|
-
if (
|
|
172
|
+
if (reqs.length) text = text.replace(reqReG, '');
|
|
174
173
|
text = text.replace(/\s+/g, ' ').trim();
|
|
175
|
-
|
|
174
|
+
// `req` stays as alias of the first id — older consumers keep working.
|
|
175
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(reqs.length ? { req: reqs[0], reqs } : {}) });
|
|
176
176
|
}
|
|
177
177
|
return tasks;
|
|
178
178
|
}
|
|
@@ -419,7 +419,7 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
let reqIds = [];
|
|
422
|
-
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).
|
|
422
|
+
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).flatMap((t) => t.reqs ?? []))]; } catch { /* sem tarefas */ }
|
|
423
423
|
|
|
424
424
|
ensureDir(join(vaultBase, chDir, ARCHIVE_DIR));
|
|
425
425
|
try {
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
41
41
|
let tasks = [];
|
|
42
42
|
let tarefasMd = '';
|
|
43
43
|
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
|
|
44
|
-
const reqIds = [...new Set(tasks.
|
|
44
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
45
45
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
46
46
|
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
47
47
|
if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, dirname, join, relative } from 'path';
|
|
4
4
|
import { getLocale } from './locale.mjs';
|
|
5
|
+
import { resolveProjectVault } from '../src/project-vault.mjs';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
7
|
+
// Deprecated export kept for consumers that imported it before 0.39.0. Automatic
|
|
8
|
+
// hooks never use this fallback: an unbound project fails closed.
|
|
8
9
|
export const DEFAULT_VAULT_BASE = join(
|
|
9
10
|
process.env.USERPROFILE || process.env.HOME || process.cwd(),
|
|
10
11
|
'wendkeep-vault',
|
|
@@ -31,19 +32,12 @@ export function writeHookOutput(payload = {}) {
|
|
|
31
32
|
process.stdout.write(JSON.stringify(payload));
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
// Resolve
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// default -> DEFAULT_VAULT_BASE (~/wendkeep-vault) — a phantom vault nobody opened
|
|
35
|
+
// Resolve from explicit hook payload or the nearest project-local binding. A legacy
|
|
36
|
+
// `.claude/settings.json` registration is accepted as a migration bridge so Codex can
|
|
37
|
+
// discover projects initialized by older WendKeep versions. Global process env and the
|
|
38
|
+
// historical home fallback are intentionally excluded to prevent cross-project writes.
|
|
39
39
|
export function resolveVault(input = {}) {
|
|
40
|
-
|
|
41
|
-
return { base: process.env.OBSIDIAN_VAULT_PATH, source: 'env' };
|
|
42
|
-
}
|
|
43
|
-
if (input && input.obsidian_vault_path) {
|
|
44
|
-
return { base: input.obsidian_vault_path, source: 'payload' };
|
|
45
|
-
}
|
|
46
|
-
return { base: DEFAULT_VAULT_BASE, source: 'default' };
|
|
40
|
+
return resolveProjectVault({ input });
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export function getVaultBase(input = {}) {
|
|
@@ -60,16 +54,15 @@ export function debugLog(...args) {
|
|
|
60
54
|
process.stderr.write(`[wendkeep] ${text}\n`);
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// Returns the resolution source so callers can branch if they want.
|
|
57
|
+
// Migration warning for projects that are still discovered through Claude's local
|
|
58
|
+
// settings. Missing bindings throw before this point and are handled by each hook's
|
|
59
|
+
// fail-closed top-level catch.
|
|
67
60
|
export function warnIfDefaultVault(input = {}) {
|
|
68
61
|
const { base, source } = resolveVault(input);
|
|
69
|
-
if (source === '
|
|
62
|
+
if (source === 'legacy-project-settings') {
|
|
70
63
|
process.stderr.write(
|
|
71
|
-
`[wendkeep]
|
|
72
|
-
'Rode `wendkeep init`
|
|
64
|
+
`[wendkeep] Configuração legada do vault detectada em .claude/settings.json ("${base}"). `
|
|
65
|
+
+ 'Rode `wendkeep init --yes` para criar .wendkeep.json; Codex e Claude continuarão no mesmo vault.\n',
|
|
73
66
|
);
|
|
74
67
|
}
|
|
75
68
|
return source;
|
package/hooks/sensors-core.mjs
CHANGED
|
@@ -2,15 +2,37 @@
|
|
|
2
2
|
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
3
|
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
9
|
+
return loadSensorsDetailed(projectRoot, file).sensors;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Missing config and broken config are different failures: absent file usually means
|
|
13
|
+
// wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
|
|
14
|
+
// both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
|
|
15
|
+
export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
|
|
16
|
+
const path = join(projectRoot, file);
|
|
17
|
+
if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
|
|
9
18
|
try {
|
|
10
|
-
const data = JSON.parse(readFileSync(
|
|
11
|
-
return Array.isArray(data.sensors) ? data.sensors : [];
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
19
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { sensors: [], missing: false, error: e.message, path };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Climb the directory tree looking for a project marker (wendkeep.sensors.json or
|
|
27
|
+
// .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
|
|
28
|
+
// across commands, so verify is often run from a subdirectory.
|
|
29
|
+
export function findProjectRoot(startDir) {
|
|
30
|
+
let dir = resolve(startDir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) return null;
|
|
35
|
+
dir = parent;
|
|
14
36
|
}
|
|
15
37
|
}
|
|
16
38
|
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -22,15 +22,29 @@ export const MANAGED_SPEC_MARKER = '<!-- wendkeep:managed-spec — generated fro
|
|
|
22
22
|
// Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
|
|
23
23
|
const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
|
|
24
24
|
|
|
25
|
+
// Single source of truth for requirement-id shape — task tags ([req:ID]) and spec
|
|
26
|
+
// headings MUST agree, or coverage silently misses (multi-segment ids like API-AUTH-2).
|
|
27
|
+
export const REQ_ID_RE_SRC = '[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\\d+';
|
|
28
|
+
|
|
29
|
+
// Orphan diagnostics must teach the fix, not just name the ids — the heading format
|
|
30
|
+
// is the most common cause and lives only here.
|
|
31
|
+
export function formatOrphanReqs(ids) {
|
|
32
|
+
const list = ids.join(', ');
|
|
33
|
+
const ex = ids[0] || 'GATE-1';
|
|
34
|
+
return `requisito(s) órfão(s) na spec efetiva: ${list} — heading esperado no spec.md da change: "### Requisito: ${ex} — <nome>" (ou só "### Requisito: ${ex}")`;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export function parseRequirements(md) {
|
|
26
38
|
const text = String(md);
|
|
27
39
|
const matches = [...text.matchAll(REQ_RE)];
|
|
28
40
|
const reqs = [];
|
|
29
41
|
for (let i = 0; i < matches.length; i += 1) {
|
|
30
42
|
const raw = matches[i][1].trim();
|
|
31
|
-
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"
|
|
32
|
-
|
|
33
|
-
const
|
|
43
|
+
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>" or a bare
|
|
44
|
+
// "<ID>"; else the whole text. Bare ids keep specs writable without the em-dash.
|
|
45
|
+
const idM = raw.match(new RegExp(`^(${REQ_ID_RE_SRC})\\s*—\\s*(.+)$`));
|
|
46
|
+
const bare = idM ? null : raw.match(new RegExp(`^(${REQ_ID_RE_SRC})$`));
|
|
47
|
+
const id = idM ? idM[1] : bare ? bare[1] : null;
|
|
34
48
|
const name = idM ? idM[2].trim() : raw;
|
|
35
49
|
const start = matches[i].index + matches[i][0].length;
|
|
36
50
|
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"node": ">=18"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
-
"check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs",
|
|
22
|
+
"check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs",
|
|
23
23
|
"test": "node --test",
|
|
24
24
|
"release": "node scripts/release.mjs",
|
|
25
25
|
"release:dry": "node scripts/release.mjs --dry-run"
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"wendkeep": "^0.
|
|
49
|
+
"wendkeep": "^0.39.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/change.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
scaffoldPlaceholders,
|
|
18
18
|
} from '../hooks/change-core.mjs';
|
|
19
19
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
20
|
-
import { buildEffectiveRequirementPackage, evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
20
|
+
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
21
21
|
import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
22
22
|
import { getLocale } from '../hooks/locale.mjs';
|
|
23
23
|
|
|
@@ -147,13 +147,13 @@ export function runChange(argv) {
|
|
|
147
147
|
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
148
148
|
process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
|
|
149
149
|
for (const t of tasks) {
|
|
150
|
-
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.
|
|
150
|
+
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${(t.reqs ?? []).map((r) => ` [req:${r}]`).join('')}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
|
|
151
151
|
}
|
|
152
152
|
let evidence = null;
|
|
153
153
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
154
154
|
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
155
155
|
else process.stdout.write('evidencia: ausente\n');
|
|
156
|
-
const reqIds = [...new Set(tasks.
|
|
156
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
157
157
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
158
158
|
if (effective.errors.length || effective.missing.length) {
|
|
159
159
|
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
@@ -239,10 +239,10 @@ export function runChange(argv) {
|
|
|
239
239
|
return { ok: false, failing: ['evidência stale (tarefas.md mudou desde o último verify) — rode `wendkeep verify` de novo'] };
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
const reqIds = [...new Set(tasks.
|
|
242
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
243
243
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
244
244
|
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
245
|
-
if (effective.missing.length) return { ok: false, failing: [
|
|
245
|
+
if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
|
|
246
246
|
let evidence = [];
|
|
247
247
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
248
248
|
const s = evaluateGate(evidence, required);
|
package/src/doctor.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// harness integrity check (hooks/harness-doctor.mjs). Exits 1 on any error.
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { checkHarness } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
import { checkSyncDefs } from './sync-defs.mjs';
|
|
9
|
+
import { resolveProjectVault } from './project-vault.mjs';
|
|
9
10
|
|
|
10
11
|
export function runDoctor(argv) {
|
|
11
12
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -23,19 +24,28 @@ export function runDoctor(argv) {
|
|
|
23
24
|
else passthrough.push(a);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const projectRoot = resolve(project || process.cwd());
|
|
28
|
+
let resolution;
|
|
29
|
+
try {
|
|
30
|
+
resolution = resolveProjectVault({
|
|
31
|
+
startDir: projectRoot,
|
|
32
|
+
explicitVault: vault || '',
|
|
33
|
+
validateIdentity: !vault,
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
process.stderr.write(`wendkeep doctor: ${error.message}\n`);
|
|
30
37
|
process.exit(2);
|
|
31
38
|
}
|
|
32
|
-
const vaultBase =
|
|
33
|
-
|
|
39
|
+
const vaultBase = resolution.base;
|
|
40
|
+
process.stdout.write(`[vault] ${resolution.source}: ${vaultBase} (project: ${projectRoot})\n`);
|
|
41
|
+
if (resolution.source === 'legacy-project-settings') {
|
|
42
|
+
process.stdout.write(' ! migração pendente: rode `wendkeep init --project . --vault "<vault>" --yes` para criar .wendkeep.json\n');
|
|
43
|
+
}
|
|
34
44
|
|
|
35
45
|
// 1. Session/vault integrity (existing check).
|
|
36
46
|
let healthStatus = 0;
|
|
37
47
|
if (existsSync(hookFile)) {
|
|
38
|
-
const r = spawnSync(process.execPath, [hookFile, ...passthrough], { stdio: 'inherit'
|
|
48
|
+
const r = spawnSync(process.execPath, [hookFile, ...passthrough, '--vault', vaultBase], { stdio: 'inherit' });
|
|
39
49
|
healthStatus = r.status ?? 0;
|
|
40
50
|
}
|
|
41
51
|
|
package/src/import.mjs
CHANGED
|
@@ -12,7 +12,22 @@ function opt(argv, name) {
|
|
|
12
12
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const KNOWN_FLAGS = new Set([
|
|
16
|
+
'--vault', '--project', '--source', '--from', '--codex-from', '--since', '--limit',
|
|
17
|
+
'--dry-run', '--json', '--rescan-decisions', '--stamp-ids', '--help', '-h',
|
|
18
|
+
]);
|
|
19
|
+
|
|
15
20
|
export function runImportCli(argv) {
|
|
21
|
+
// Import writes to the vault; an unrecognized flag must never fall through to the
|
|
22
|
+
// destructive default (--source all). Fail fast, point at --help.
|
|
23
|
+
for (const a of argv) {
|
|
24
|
+
if (!a.startsWith('-')) continue;
|
|
25
|
+
const name = a.includes('=') ? a.slice(0, a.indexOf('=')) : a;
|
|
26
|
+
if (!KNOWN_FLAGS.has(name)) {
|
|
27
|
+
process.stderr.write(`wendkeep import: flag desconhecida "${name}" (use --help)\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
17
32
|
if (!vaultRaw) { process.stderr.write('wendkeep import: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
18
33
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
package/src/init.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `wendkeep init` — cross-platform replacement for setup-vault.ps1.
|
|
2
|
-
// Creates the vault taxonomy, NON-DESTRUCTIVELY
|
|
3
|
-
//
|
|
2
|
+
// Creates the vault taxonomy + provider-neutral project binding, NON-DESTRUCTIVELY
|
|
3
|
+
// merges the session hooks into .claude/settings.json, and adds the mcpvault server to
|
|
4
4
|
// .mcp.json. Idempotent: re-running only adds what is missing.
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
@@ -41,6 +41,7 @@ import { seedWkSkills } from './skills-seed.mjs';
|
|
|
41
41
|
import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
|
|
42
42
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
43
43
|
import { adoptSpecsState, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
|
|
44
|
+
import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
|
|
44
45
|
|
|
45
46
|
function parseArgs(argv) {
|
|
46
47
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -281,7 +282,7 @@ const MESSAGES = {
|
|
|
281
282
|
readmeCreated: ', README.md criado', viewsNote: (n) => `, ${n} view(s) + dashboard`,
|
|
282
283
|
defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
|
|
283
284
|
settingsBadJson: (p) => ` [2/4] settings.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
|
|
284
|
-
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wirados
|
|
285
|
+
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wirados; .wendkeep.json é o vínculo compartilhado${bak})`,
|
|
285
286
|
mcpBadJson: (p) => ` [3/4] .mcp.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
|
|
286
287
|
mcp: (verb, names, bak) => ` [3/4] .mcp.json ${verb} (${names}${bak})`,
|
|
287
288
|
mcpSkipped: ' [3/4] .mcp.json ignorado (--no-mcp, sem companions MCP)',
|
|
@@ -306,7 +307,7 @@ const MESSAGES = {
|
|
|
306
307
|
readmeCreated: ', README.md created', viewsNote: (n) => `, ${n} view(s) + dashboard`,
|
|
307
308
|
defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
|
|
308
309
|
settingsBadJson: (p) => ` [2/4] settings.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
309
|
-
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired
|
|
310
|
+
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired; .wendkeep.json is the shared binding${bak})`,
|
|
310
311
|
mcpBadJson: (p) => ` [3/4] .mcp.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
311
312
|
mcp: (verb, names, bak) => ` [3/4] .mcp.json ${verb} (${names}${bak})`,
|
|
312
313
|
mcpSkipped: ' [3/4] .mcp.json skipped (--no-mcp, no MCP companions)',
|
|
@@ -334,9 +335,14 @@ export function parseLocaleAnswer(ans) {
|
|
|
334
335
|
return 'pt-BR';
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
// The vault path this project was set up with
|
|
338
|
-
//
|
|
338
|
+
// The vault path this project was set up with. New installs use the provider-neutral
|
|
339
|
+
// `.wendkeep.json`; `.claude/settings.json` remains a local migration bridge for projects
|
|
340
|
+
// initialized before 0.39.0.
|
|
339
341
|
export function detectRegisteredVault(projectPath) {
|
|
342
|
+
try {
|
|
343
|
+
const binding = readProjectBinding(projectPath);
|
|
344
|
+
if (binding?.base) return binding.base;
|
|
345
|
+
} catch { /* init will surface/repair through an explicit --vault */ }
|
|
340
346
|
try {
|
|
341
347
|
const s = JSON.parse(readFileSync(join(projectPath, '.claude', 'settings.json'), 'utf8'));
|
|
342
348
|
const v = s && s.env && s.env.OBSIDIAN_VAULT_PATH;
|
|
@@ -357,8 +363,8 @@ export async function runInit(argv) {
|
|
|
357
363
|
const projectPath = resolve(args.project || process.cwd());
|
|
358
364
|
const log = (s) => process.stdout.write(`${s}\n`);
|
|
359
365
|
|
|
360
|
-
// Recognize an already-configured project
|
|
361
|
-
//
|
|
366
|
+
// Recognize an already-configured project through `.wendkeep.json`, or through the
|
|
367
|
+
// project-local Claude setting during migration. Locale remains locked in vault config.
|
|
362
368
|
// On re-run (e.g. after `npm i -D wendkeep@latest`) we reuse both and SKIP the language + vault
|
|
363
369
|
// prompts — asking again risks a divergent vault from a mistyped name. `--vault` / `--locale`
|
|
364
370
|
// override; the vault question is a once-per-project thing.
|
|
@@ -393,6 +399,10 @@ export async function runInit(argv) {
|
|
|
393
399
|
}
|
|
394
400
|
vaultPath = isAbsolute(vaultPath) ? vaultPath : resolve(projectPath, vaultPath);
|
|
395
401
|
|
|
402
|
+
// Persist the provider-neutral binding before any hook/config delivery. This also
|
|
403
|
+
// claims the vault identity and rejects accidental cross-project graph contamination.
|
|
404
|
+
bindProjectVault({ projectRoot: projectPath, vaultPath });
|
|
405
|
+
|
|
396
406
|
// Companion plugins/MCP selection. --no-companions wins; --companions <csv> is
|
|
397
407
|
// explicit; an interactive TTY gets a multi-choice prompt (context-mode pre-checked);
|
|
398
408
|
// otherwise the non-interactive default (context-mode only).
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const PROJECT_CONFIG_FILE = '.wendkeep.json';
|
|
13
|
+
export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
|
|
14
|
+
export const PROJECT_CONFIG_SCHEMA = 1;
|
|
15
|
+
|
|
16
|
+
function json(path) {
|
|
17
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
const wrapped = new Error(`Configuração WendKeep inválida em "${path}": ${error.message}`);
|
|
20
|
+
wrapped.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
21
|
+
throw wrapped;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function atomicJson(path, value) {
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
28
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
|
|
29
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
30
|
+
writeFileSync(temp, content, 'utf8');
|
|
31
|
+
renameSync(temp, path);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function startDirectory(value) {
|
|
36
|
+
const candidate = resolve(String(value || process.cwd()));
|
|
37
|
+
try { return statSync(candidate).isFile() ? dirname(candidate) : candidate; }
|
|
38
|
+
catch { return candidate; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkParents(start) {
|
|
42
|
+
const result = [];
|
|
43
|
+
let current = startDirectory(start);
|
|
44
|
+
const root = parse(current).root;
|
|
45
|
+
while (true) {
|
|
46
|
+
result.push(current);
|
|
47
|
+
if (current === root) break;
|
|
48
|
+
const parent = dirname(current);
|
|
49
|
+
if (parent === current) break;
|
|
50
|
+
current = parent;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inputStart(input = {}, fallback = '') {
|
|
56
|
+
return input.cwd
|
|
57
|
+
|| input.project_dir
|
|
58
|
+
|| input.projectDir
|
|
59
|
+
|| input.workspace?.cwd
|
|
60
|
+
|| process.env.CLAUDE_PROJECT_DIR
|
|
61
|
+
|| fallback
|
|
62
|
+
|| process.cwd();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function vaultFromConfig(projectRoot, config) {
|
|
66
|
+
if (!config || config.schemaVersion !== PROJECT_CONFIG_SCHEMA || !config.projectId || !config.vault) {
|
|
67
|
+
const error = new Error(
|
|
68
|
+
`Configuração incompleta em "${join(projectRoot, PROJECT_CONFIG_FILE)}". `
|
|
69
|
+
+ 'Rode `wendkeep init --project <path> --vault <path>`.',
|
|
70
|
+
);
|
|
71
|
+
error.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return isAbsolute(config.vault) ? resolve(config.vault) : resolve(projectRoot, config.vault);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readProjectBinding(projectRoot) {
|
|
78
|
+
const root = resolve(projectRoot);
|
|
79
|
+
const path = join(root, PROJECT_CONFIG_FILE);
|
|
80
|
+
if (!existsSync(path)) return null;
|
|
81
|
+
const config = json(path);
|
|
82
|
+
return { config, configPath: path, projectRoot: root, base: vaultFromConfig(root, config) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function findProjectBinding(start) {
|
|
86
|
+
for (const projectRoot of walkParents(start)) {
|
|
87
|
+
const found = readProjectBinding(projectRoot);
|
|
88
|
+
if (found) return found;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function findLegacyProjectVault(start) {
|
|
94
|
+
for (const projectRoot of walkParents(start)) {
|
|
95
|
+
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
96
|
+
if (!existsSync(settingsPath)) continue;
|
|
97
|
+
try {
|
|
98
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
99
|
+
const raw = settings?.env?.OBSIDIAN_VAULT_PATH;
|
|
100
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
101
|
+
return {
|
|
102
|
+
base: isAbsolute(raw) ? resolve(raw) : resolve(projectRoot, raw),
|
|
103
|
+
projectRoot,
|
|
104
|
+
source: 'legacy-project-settings',
|
|
105
|
+
configPath: settingsPath,
|
|
106
|
+
projectId: '',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
} catch { /* init/doctor explicam JSON inválido; descoberta segue procurando */ }
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readVaultMarker(vaultPath) {
|
|
115
|
+
const markerPath = join(resolve(vaultPath), ...PROJECT_MARKER_REL.split('/'));
|
|
116
|
+
if (!existsSync(markerPath)) return null;
|
|
117
|
+
return { marker: json(markerPath), markerPath };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateMarker(result) {
|
|
121
|
+
const found = readVaultMarker(result.base);
|
|
122
|
+
if (!found) {
|
|
123
|
+
const error = new Error(
|
|
124
|
+
`O vault "${result.base}" ainda não possui ${PROJECT_MARKER_REL}. `
|
|
125
|
+
+ `Rode \`wendkeep init --project "${result.projectRoot}" --vault "${result.base}" --yes\`.`,
|
|
126
|
+
);
|
|
127
|
+
error.code = 'WENDKEEP_VAULT_MARKER_MISSING';
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (found.marker?.projectId !== result.projectId) {
|
|
131
|
+
const error = new Error(
|
|
132
|
+
`Vault de outro projeto: configuração "${result.projectId}" aponta para marcador `
|
|
133
|
+
+ `"${found.marker?.projectId || 'ausente'}" em "${found.markerPath}".`,
|
|
134
|
+
);
|
|
135
|
+
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveProjectVault({
|
|
141
|
+
input = {},
|
|
142
|
+
startDir = '',
|
|
143
|
+
explicitVault = '',
|
|
144
|
+
allowLegacySettings = true,
|
|
145
|
+
validateIdentity = true,
|
|
146
|
+
} = {}) {
|
|
147
|
+
const start = inputStart(input, startDir);
|
|
148
|
+
const explicit = explicitVault || input?.obsidian_vault_path;
|
|
149
|
+
if (explicit) {
|
|
150
|
+
return {
|
|
151
|
+
base: isAbsolute(explicit) ? resolve(explicit) : resolve(startDirectory(start), explicit),
|
|
152
|
+
source: explicitVault ? 'explicit' : 'payload',
|
|
153
|
+
projectRoot: startDirectory(start),
|
|
154
|
+
projectId: '',
|
|
155
|
+
configPath: '',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const binding = findProjectBinding(start);
|
|
160
|
+
if (binding) {
|
|
161
|
+
const result = {
|
|
162
|
+
base: binding.base,
|
|
163
|
+
source: 'project-config',
|
|
164
|
+
projectRoot: binding.projectRoot,
|
|
165
|
+
projectId: binding.config.projectId,
|
|
166
|
+
configPath: binding.configPath,
|
|
167
|
+
};
|
|
168
|
+
if (validateIdentity) validateMarker(result);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (allowLegacySettings) {
|
|
173
|
+
const legacy = findLegacyProjectVault(start);
|
|
174
|
+
if (legacy) return legacy;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const error = new Error(
|
|
178
|
+
`Nenhum vault WendKeep vinculado ao projeto em "${startDirectory(start)}". `
|
|
179
|
+
+ `Crie ${PROJECT_CONFIG_FILE} com \`wendkeep init --project "${startDirectory(start)}" --vault <path> --yes\`.`,
|
|
180
|
+
);
|
|
181
|
+
error.code = 'WENDKEEP_VAULT_UNCONFIGURED';
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function portableVaultPath(projectRoot, vaultPath) {
|
|
186
|
+
const rel = relative(projectRoot, vaultPath);
|
|
187
|
+
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return rel.replaceAll('\\', '/');
|
|
188
|
+
return vaultPath;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function bindProjectVault({ projectRoot, vaultPath }) {
|
|
192
|
+
const root = resolve(projectRoot);
|
|
193
|
+
const base = isAbsolute(vaultPath) ? resolve(vaultPath) : resolve(root, vaultPath);
|
|
194
|
+
const existing = readProjectBinding(root);
|
|
195
|
+
const existingMarker = readVaultMarker(base);
|
|
196
|
+
const projectId = existing?.config?.projectId || existingMarker?.marker?.projectId || randomUUID();
|
|
197
|
+
|
|
198
|
+
if (existingMarker?.marker?.projectId && existingMarker.marker.projectId !== projectId) {
|
|
199
|
+
const error = new Error(
|
|
200
|
+
`Não é seguro vincular "${root}" ao vault de outro projeto: `
|
|
201
|
+
+ `esperado "${projectId}", encontrado "${existingMarker.marker.projectId}".`,
|
|
202
|
+
);
|
|
203
|
+
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
mkdirSync(join(base, '.brain'), { recursive: true });
|
|
208
|
+
const config = {
|
|
209
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
210
|
+
projectId,
|
|
211
|
+
vault: portableVaultPath(root, base),
|
|
212
|
+
};
|
|
213
|
+
const marker = {
|
|
214
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
215
|
+
projectId,
|
|
216
|
+
projectName: basename(root),
|
|
217
|
+
};
|
|
218
|
+
atomicJson(join(base, ...PROJECT_MARKER_REL.split('/')), marker);
|
|
219
|
+
atomicJson(join(root, PROJECT_CONFIG_FILE), config);
|
|
220
|
+
return { base, projectRoot: root, projectId, config, marker };
|
|
221
|
+
}
|
package/src/skills-seed.mjs
CHANGED
|
@@ -35,15 +35,18 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
35
35
|
Antes de implementar, resolva \`spec_impact\` na proposta:
|
|
36
36
|
- \`required\`: liste a capability em \`specs:\` e preencha
|
|
37
37
|
\`specs/<capability>/spec.md\` com ADDED/MODIFIED/REMOVED; ligue tarefas com \`[req:ID]\`.
|
|
38
|
+
Heading de requisito: \`### Requisito: <ID> — <nome>\` (ou só \`### Requisito: <ID>\`);
|
|
39
|
+
o ID é a identidade (ex.: \`GATE-1\`, \`API-AUTH-2\`).
|
|
38
40
|
- \`none\`: registre uma justificativa real em \`spec_impact_reason\`.
|
|
39
41
|
\`pending\` nunca é estado pronto para implementação ou archive.
|
|
40
42
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
41
43
|
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
42
44
|
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
43
45
|
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
44
|
-
quando a change mexe numa capability.
|
|
46
|
+
quando a change mexe numa capability. Uma tarefa pode declarar vários
|
|
47
|
+
\`[req:]\` — todos contam na cobertura. Toda autoria de spec ocorre somente em
|
|
45
48
|
\`08-Mudanças/<slug>/specs/<capability>/spec.md\`; \`07-Specs\` é gerado/read-only.
|
|
46
|
-
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
49
|
+
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [req:MEM-2] [sensor:memory-validation]\`.
|
|
47
50
|
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
48
51
|
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
49
52
|
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
@@ -259,8 +262,11 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
259
262
|
Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
|
|
260
263
|
\`specs:\` plus a real \`specs/<capability>/spec.md\` delta and \`[req:ID]\` links; \`none\`
|
|
261
264
|
needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
|
|
265
|
+
Requirement heading: \`### Requirement: <ID> — <name>\` (or bare \`### Requirement: <ID>\`);
|
|
266
|
+
the ID is the identity (e.g. \`GATE-1\`, \`API-AUTH-2\`).
|
|
262
267
|
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
263
|
-
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies
|
|
268
|
+
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies;
|
|
269
|
+
a task may declare several \`[req:]\` tags — all of them count toward coverage).
|
|
264
270
|
Author specs only in \`08-Changes/<slug>/specs/\`; \`07-Specs\` is generated/read-only.
|
|
265
271
|
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
266
272
|
the verification package.
|
package/src/spec.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function runSpec(argv) {
|
|
|
49
49
|
tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
|
|
50
50
|
}
|
|
51
51
|
catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
|
|
52
|
-
const reqIds = [...new Set(tasks.
|
|
52
|
+
const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
|
|
53
53
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
54
54
|
if (effective.errors.length) {
|
|
55
55
|
process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
|
package/src/vault-readme.mjs
CHANGED
|
@@ -52,7 +52,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, loca
|
|
|
52
52
|
const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
|
|
53
53
|
const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
|
|
54
54
|
if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
|
|
55
|
-
access.push('- **Hooks:**
|
|
55
|
+
access.push('- **Hooks:** Codex and Claude Code call `npx wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
|
|
56
56
|
return `# Obsidian vault — ${name}
|
|
57
57
|
|
|
58
58
|
> Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
|
|
@@ -84,7 +84,7 @@ ${access.join('\n')}
|
|
|
84
84
|
const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
|
|
85
85
|
const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
|
|
86
86
|
if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
|
|
87
|
-
access.push('- **Hooks:**
|
|
87
|
+
access.push('- **Hooks:** Codex e Claude Code chamam `npx wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
|
|
88
88
|
return `# Vault Obsidian — ${name}
|
|
89
89
|
|
|
90
90
|
> Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
|
package/src/verify.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { loadSensorsDetailed, findProjectRoot, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
import {
|
|
9
9
|
buildEffectiveRequirementPackage,
|
|
10
10
|
captureSpecBaseline,
|
|
11
|
+
formatOrphanReqs,
|
|
11
12
|
tasksHashOf,
|
|
12
13
|
} from '../hooks/spec-core.mjs';
|
|
13
14
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
@@ -29,7 +30,9 @@ export function runVerify(argv) {
|
|
|
29
30
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
30
31
|
if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
31
32
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
32
|
-
|
|
33
|
+
// --project wins; otherwise climb from cwd to the nearest project marker (agent shells
|
|
34
|
+
// keep their cwd across commands, so verify from a subdirectory is a recurring miss).
|
|
35
|
+
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
33
36
|
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
34
37
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
35
38
|
|
|
@@ -39,7 +42,15 @@ export function runVerify(argv) {
|
|
|
39
42
|
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|
|
40
43
|
|
|
41
44
|
const ids = requiredSensors(parseTasks(tarefas));
|
|
42
|
-
const
|
|
45
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
46
|
+
if (loaded.error) {
|
|
47
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json inválido em ${loaded.path}: ${loaded.error}\n`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
if (loaded.missing && ids.length) {
|
|
51
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
|
|
52
|
+
}
|
|
53
|
+
const sensors = loaded.sensors;
|
|
43
54
|
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
44
55
|
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
45
56
|
// Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
|
|
@@ -90,7 +101,7 @@ export function runVerify(argv) {
|
|
|
90
101
|
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
91
102
|
if (argv.includes('--deep')) {
|
|
92
103
|
const tasks = parseTasks(tarefas);
|
|
93
|
-
const reqIds = [...new Set(tasks.
|
|
104
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
94
105
|
const tasksHash = tasksHashOf(tarefas);
|
|
95
106
|
captureSpecBaseline(vaultBase, changeDir);
|
|
96
107
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
@@ -99,7 +110,7 @@ export function runVerify(argv) {
|
|
|
99
110
|
process.exit(1);
|
|
100
111
|
}
|
|
101
112
|
if (effective.missing.length) {
|
|
102
|
-
process.stderr.write(`verify --deep:
|
|
113
|
+
process.stderr.write(`verify --deep: ${formatOrphanReqs(effective.missing)}\n`);
|
|
103
114
|
process.exit(1);
|
|
104
115
|
}
|
|
105
116
|
const pkg = {
|
|
@@ -116,7 +127,7 @@ export function runVerify(argv) {
|
|
|
116
127
|
body: req.body,
|
|
117
128
|
};
|
|
118
129
|
}),
|
|
119
|
-
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
130
|
+
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
|
|
120
131
|
sensors: evidence,
|
|
121
132
|
};
|
|
122
133
|
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|