wendkeep 0.17.0 → 0.20.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 CHANGED
@@ -4,6 +4,68 @@ 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.20.0] — 2026-07-06
8
+
9
+ Richer skills: bundled templates (multi-file).
10
+
11
+ ### Added
12
+ - The process skills now ship **bundled templates** next to their `SKILL.md`, delivered together
13
+ by `sync-defs` (the whole skill folder is copied) and auto-delivered by `init`. The model reads
14
+ them on demand — depth without bloating `SKILL.md`:
15
+ - **wk-verify** → `spec-reviewer-prompt.md` (the prompt to hand a fresh read-only verifier
16
+ sub-agent) + `verdict-template.json` (the exact `verdict.json` shape).
17
+ - **wk-planning** → `plan-template.md` (file map + bite-sized TDD task structure).
18
+ - **wk-brainstorming** → `design-template.md` (context, approaches, signed-off assumptions,
19
+ out-of-scope table, acceptance).
20
+ - pt-BR and en variants; the prose templates follow the vault locale, the JSON is shared.
21
+
22
+ ### Notes
23
+ - Subagents stay the **native harness's** job. wendkeep ships the verifier **prompt** (the agent
24
+ spawns a read-only sub-agent via its own Task/Agent tool) and captures subagent telemetry — it
25
+ does not orchestrate spawning. So the reviewer is a template, not a Claude-only
26
+ `.claude/agents/*.md`, which keeps it agent-agnostic.
27
+
28
+ ### Upgrade
29
+ - `npm update wendkeep`, then `wendkeep init` (or `wendkeep sync-defs`) to get the templates
30
+ alongside your existing skills. Non-destructive — existing `SKILL.md` files are never overwritten.
31
+
32
+ ## [0.19.0] — 2026-07-06
33
+
34
+ Fix: memory + active-change injection wired by default.
35
+
36
+ ### Fixed
37
+ - `wendkeep init` now wires the **`brain-inject`** hook into SessionStart (ordered *before*
38
+ `session-start`), so every session gets `<brain_memory>` injected: CORE + DIGEST + the
39
+ **active change** (proposal + open tasks) + project lessons. Previously the default hook set was
40
+ only `session-start` / `session-stop` / `session-ensure` — the memory/change injector existed
41
+ (`wendkeep hook brain-inject`) but wasn't wired, so the "the change is injected at the next
42
+ SessionStart" promise (the `wk-workflow` skill and the README) didn't actually hold on a fresh
43
+ install. matcher `startup|clear|compact` re-injects after a compaction or clear, not only on a
44
+ cold startup.
45
+
46
+ ### Upgrade
47
+ - Existing installs pick it up by re-running `wendkeep init --force` (idempotent — it only adds the
48
+ missing hook), or by adding `npx wendkeep hook brain-inject` to the SessionStart hooks manually.
49
+
50
+ ## [0.18.0] — 2026-07-06
51
+
52
+ Session identity in the note.
53
+
54
+ ### Added
55
+ - Session notes now carry **`session_id`** in their frontmatter — both live capture and import,
56
+ Claude and Codex. Pairs with the existing `provider:` field so every note self-identifies
57
+ (which conversation, which agent) without consulting the registry.
58
+ - **`wendkeep import --stamp-ids`** — backfill `session_id` into existing notes from the
59
+ `SESSION_REGISTRY` (for notes captured or imported before the field existed). Idempotent;
60
+ only touches notes missing the field.
61
+ - Import dedup now also scans existing notes' `session_id` (`capturedSessionIds` = registry ∪
62
+ note frontmatter), so a session that already has a note on disk is never re-imported even if
63
+ the registry was reset or lost.
64
+
65
+ ### Changed
66
+ - `buildSessionContent` accepts a `sessionId`; the SessionStart hook (all three create/recreate
67
+ paths) and `importSession` thread the id through, so a note records its identity at creation.
68
+
7
69
  ## [0.17.0] — 2026-07-06
8
70
 
9
71
  Retroactive memory, now agent-agnostic (Codex).
package/README.md CHANGED
@@ -113,7 +113,8 @@ wendkeep import --vault .myproject-vault --source codex # just Codex
113
113
  ```
114
114
 
115
115
  - **Both agents by default** (`--source all`). Claude sessions come from `~/.claude/projects/<slug>/`; Codex rollouts from `~/.codex/sessions/**`, scoped to this project by the `cwd` recorded in each session (case- and separator-insensitive, subdirs included). Narrow with `--source claude` / `--source codex`.
116
- - **Deduped** by `session_id` against the vault's `SESSION_REGISTRY` only sessions not already present are imported, and it never overwrites an existing note. Re-running is a no-op.
116
+ - Every note records its **`session_id`** and **`provider`** in frontmatter (live capture and import alike). Backfill older notes with `wendkeep import --stamp-ids` (fills the id from the registry; idempotent).
117
+ - **Deduped** by `session_id` against the vault's `SESSION_REGISTRY` **and** existing notes' frontmatter — only sessions not already present are imported, and it never overwrites an existing note. Re-running is a no-op.
117
118
  - **`--from <dir>`** / **`--codex-from <dir>`** point at the transcript folders explicitly (use if the auto-derived path misses). Also: `--since <date>`, `--limit <n>`, `--json`.
118
119
  - Once imported, `wendkeep cost` aggregates your entire history — retroactively, across both agents.
119
120
 
@@ -4,7 +4,7 @@
4
4
  // dated folder, deduped by session_id against the vault's SESSION_REGISTRY. This is an offline
5
5
  // replay of the live capture flow — same skeleton, same iteration blocks, same cost/subagent
6
6
  // telemetry, same finalize — so an imported note is indistinguishable from a captured one.
7
- import { existsSync, readdirSync, writeFileSync, openSync, readSync, closeSync } from 'fs';
7
+ import { existsSync, readdirSync, writeFileSync, readFileSync, openSync, readSync, closeSync } from 'fs';
8
8
  import { basename, join } from 'path';
9
9
  import {
10
10
  parseTranscript,
@@ -19,6 +19,7 @@ import { createLinkedNotes } from './linked-notes.mjs';
19
19
  import { updateSessionUsage } from './token-usage.mjs';
20
20
  import { upsertSubagentUsage } from './subagent-usage.mjs';
21
21
  import { readSessionRegistry, upsertSessionRegistry, formatLocalIso, formatDate } from './obsidian-common.mjs';
22
+ import { getLocale } from './locale.mjs';
22
23
 
23
24
  // Claude encodes a project's absolute path as its `.claude/projects` dir name by replacing each
24
25
  // path separator and the drive colon with '-'. `C:\GitHub\WendKeep` -> `C--GitHub-WendKeep`.
@@ -69,45 +70,71 @@ function cwdMatchesProject(cwd, projectPath) {
69
70
  return c === proj || c.startsWith(`${proj}/`);
70
71
  }
71
72
 
72
- // Recursively collect *.jsonl paths (manual walk: readdir recursive lands in Node 20; floor is 18).
73
- function walkJsonl(dir, out = []) {
73
+ // Recursively collect paths matching `re` (manual walk: readdir recursive lands in Node 20; floor is 18).
74
+ function walkFiles(dir, re, out = []) {
74
75
  let entries;
75
76
  try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
76
77
  for (const e of entries) {
77
78
  const p = join(dir, e.name);
78
- if (e.isDirectory()) walkJsonl(p, out);
79
- else if (/\.jsonl$/i.test(e.name)) out.push(p);
79
+ if (e.isDirectory()) walkFiles(p, re, out);
80
+ else if (re.test(e.name)) out.push(p);
80
81
  }
81
82
  return out;
82
83
  }
83
84
 
84
- // Read only the leading bytes to pull the session_meta payload (id + cwd); session_meta is the
85
- // first line of a rollout, so a bounded prefix read avoids parsing multi-MB transcripts twice.
86
- function readSessionMeta(path) {
85
+ // Read only the leading bytes of a file (avoids slurping multi-MB transcripts / notes).
86
+ function readPrefix(path, bytes = 4096) {
87
87
  let fd;
88
88
  try {
89
89
  fd = openSync(path, 'r');
90
- const buf = Buffer.alloc(16384);
91
- const n = readSync(fd, buf, 0, buf.length, 0);
92
- for (const line of buf.slice(0, n).toString('utf-8').split('\n')) {
93
- if (!line.trim()) continue;
94
- let e;
95
- try { e = JSON.parse(line); } catch { return null; } // partial/oversized first line -> skip
96
- return e.type === 'session_meta' ? (e.payload || {}) : null; // meta is always line 1
97
- }
90
+ const buf = Buffer.alloc(bytes);
91
+ const n = readSync(fd, buf, 0, bytes, 0);
92
+ return buf.slice(0, n).toString('utf-8');
98
93
  } catch {
99
- return null;
94
+ return '';
100
95
  } finally {
101
96
  if (fd !== undefined) { try { closeSync(fd); } catch { /* already closed */ } }
102
97
  }
98
+ }
99
+
100
+ // Pull the session_meta payload (id + cwd). session_meta is line 1 of a rollout.
101
+ function readSessionMeta(path) {
102
+ for (const line of readPrefix(path, 16384).split('\n')) {
103
+ if (!line.trim()) continue;
104
+ let e;
105
+ try { e = JSON.parse(line); } catch { return null; } // partial/oversized first line -> skip
106
+ return e.type === 'session_meta' ? (e.payload || {}) : null;
107
+ }
103
108
  return null;
104
109
  }
105
110
 
111
+ // The `session_id` recorded in a note's frontmatter (empty when absent).
112
+ function noteSessionId(path) {
113
+ const m = readPrefix(path, 2048).match(/^session_id:\s*["']?([^"'\r\n]+)["']?\s*$/m);
114
+ return m ? m[1].trim() : '';
115
+ }
116
+
117
+ // Every session_id the vault already has a record of: the SESSION_REGISTRY plus every session
118
+ // note's frontmatter id. The registry is authoritative for wendkeep-native vaults, but scanning
119
+ // notes too keeps import safe even if the registry was reset/lost — a session with a note on
120
+ // disk is never re-imported.
121
+ export function capturedSessionIds(vaultBase) {
122
+ const ids = new Set(Object.keys(readSessionRegistry(vaultBase).sessions || {}));
123
+ try {
124
+ const sessionsDir = join(vaultBase, getLocale(vaultBase).folders.sessions);
125
+ for (const path of walkFiles(sessionsDir, /\.md$/i)) {
126
+ const id = noteSessionId(path);
127
+ if (id) ids.add(id);
128
+ }
129
+ } catch { /* registry alone is enough */ }
130
+ return ids;
131
+ }
132
+
106
133
  export function discoverCodexTranscripts(projectPath, fromDir) {
107
134
  const dir = fromDir || defaultCodexSessionsDir();
108
135
  if (!dir || !existsSync(dir)) return { dir, transcripts: [] };
109
136
  const transcripts = [];
110
- for (const path of walkJsonl(dir)) {
137
+ for (const path of walkFiles(dir, /\.jsonl$/i)) {
111
138
  const meta = readSessionMeta(path);
112
139
  if (!meta || !meta.id) continue;
113
140
  if (projectPath && !cwdMatchesProject(meta.cwd, projectPath)) continue;
@@ -147,9 +174,9 @@ export function importSession(vaultBase, txPath, opts = {}) {
147
174
  const endDate = toDate(turns.at(-1).timestamp, startDate);
148
175
  const summary = deriveSummary(tx);
149
176
 
150
- // Skeleton in the real dated folder, tagged with the transcript's own provider.
177
+ // Skeleton in the real dated folder, tagged with the transcript's own provider + session id.
151
178
  const { absPath, relPath } = allocateSessionPath(vaultBase, startDate, summary);
152
- writeFileSync(absPath, buildSessionContent({ relPath, now: startDate, summary, provider: tx.provider }), 'utf-8');
179
+ writeFileSync(absPath, buildSessionContent({ relPath, now: startDate, summary, provider: tx.provider, sessionId }), 'utf-8');
153
180
 
154
181
  // One iteration block per turn (insertIteration dedups by turn marker -> re-import safe).
155
182
  for (const turn of turns) {
@@ -186,6 +213,34 @@ export function importSession(vaultBase, txPath, opts = {}) {
186
213
  return { sessionId, relPath, turns: turns.length };
187
214
  }
188
215
 
216
+ // Stamp a missing/empty `session_id` into a note's frontmatter (only within the frontmatter
217
+ // block, right after `provider:`). Leaves a note that already has a non-empty id untouched.
218
+ function injectSessionIdFrontmatter(content, sessionId) {
219
+ const q = `"${String(sessionId).replace(/"/g, '\\"')}"`;
220
+ if (/^session_id:\s*$/m.test(content)) return content.replace(/^session_id:\s*$/m, `session_id: ${q}`);
221
+ if (/^session_id:\s*\S/m.test(content)) return content; // already has a value
222
+ if (/^provider:.*$/m.test(content)) return content.replace(/^(provider:.*)$/m, `$1\nsession_id: ${q}`);
223
+ return content.replace(/^---\s*$/m, `---\nsession_id: ${q}`);
224
+ }
225
+
226
+ // Backfill `session_id` into existing session notes from the SESSION_REGISTRY (session_file -> id).
227
+ // For notes captured/imported before the field existed. Idempotent; only touches notes missing it.
228
+ export function stampSessionIds(vaultBase) {
229
+ const reg = readSessionRegistry(vaultBase).sessions || {};
230
+ const report = { total: Object.keys(reg).length, stamped: 0, alreadyOk: 0, missingFile: 0 };
231
+ for (const [sessionId, entry] of Object.entries(reg)) {
232
+ if (!entry?.session_file) continue;
233
+ const abs = join(vaultBase, entry.session_file);
234
+ if (!existsSync(abs)) { report.missingFile++; continue; }
235
+ let content;
236
+ try { content = readFileSync(abs, 'utf-8'); } catch { report.missingFile++; continue; }
237
+ const next = injectSessionIdFrontmatter(content, sessionId);
238
+ if (next !== content) { writeFileSync(abs, next, 'utf-8'); report.stamped++; }
239
+ else { report.alreadyOk++; }
240
+ }
241
+ return report;
242
+ }
243
+
189
244
  // Import every not-yet-captured transcript from the requested source(s). Deduped by session_id
190
245
  // against the registry. Options: { projectPath, source ('all'|'claude'|'codex'), from, codexFrom,
191
246
  // since (ISO/date), limit, dryRun }. importSession is provider-agnostic, so both sources share
@@ -206,7 +261,7 @@ export function runImport(vaultBase, opts = {}) {
206
261
  codexDir = d.dir;
207
262
  transcripts.push(...d.transcripts);
208
263
  }
209
- const captured = new Set(Object.keys(readSessionRegistry(vaultBase).sessions || {}));
264
+ const captured = capturedSessionIds(vaultBase);
210
265
  const sinceMs = since ? Date.parse(since) : 0;
211
266
  const report = { source: src, claudeDir, codexDir, scanned: transcripts.length, imported: 0, skipped: 0, errors: [], sessions: [] };
212
267
 
@@ -31,7 +31,7 @@ import {
31
31
  yamlQuote,
32
32
  } from './obsidian-common.mjs';
33
33
 
34
- export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId }) {
34
+ export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
35
35
  const date = formatDate(now);
36
36
  const startedAt = formatLocalIso(now);
37
37
  const titleTime = formatTime(now).slice(0, 5);
@@ -46,6 +46,7 @@ date: ${date}
46
46
  started_at: ${startedAt}
47
47
  ended_at:
48
48
  provider: ${provider.id}
49
+ session_id: ${sessionId ? yamlQuote(sessionId) : ''}
49
50
  status: active
50
51
  summary: ${yamlQuote(summary)}
51
52
  cssclasses:
@@ -202,7 +203,7 @@ function main() {
202
203
  const knownAbs = join(vaultBase, known.session_file);
203
204
  if (!existsSync(knownAbs)) {
204
205
  ensureDir(join(vaultBase, known.session_file.split('/').slice(0, -1).join('/')));
205
- writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input) }), 'utf-8');
206
+ writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input), sessionId }), 'utf-8');
206
207
  }
207
208
  const startedAt = known.started_at || control.started_at || formatLocalIso(now);
208
209
  writeControl(vaultBase, {
@@ -244,7 +245,7 @@ function main() {
244
245
  const matchAbs = join(vaultBase, match.session_file);
245
246
  if (!existsSync(matchAbs)) {
246
247
  ensureDir(join(vaultBase, match.session_file.split('/').slice(0, -1).join('/')));
247
- writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input) }), 'utf-8');
248
+ writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input), sessionId: sessionId || match.sessionId }), 'utf-8');
248
249
  }
249
250
  const startedAt = match.started_at || control.started_at || formatLocalIso(now);
250
251
  writeControl(vaultBase, {
@@ -276,7 +277,7 @@ function main() {
276
277
  const summary = sessionSummaryFromInput(input);
277
278
  const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
278
279
  const startedAt = formatLocalIso(now);
279
- writeFileSync(absPath, buildSessionContent({ relPath, now, summary }), 'utf-8');
280
+ writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId }), 'utf-8');
280
281
  writeControl(vaultBase, {
281
282
  status: 'active',
282
283
  session_file: relPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.17.0",
3
+ "version": "0.20.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": {
package/src/import.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // by session_id). One command backfills your whole history: cost, subagents, iterations.
4
4
  import { existsSync } from 'node:fs';
5
5
  import { isAbsolute, resolve } from 'node:path';
6
- import { runImport } from '../hooks/import-sessions.mjs';
6
+ import { runImport, stampSessionIds } from '../hooks/import-sessions.mjs';
7
7
 
8
8
  function opt(argv, name) {
9
9
  const i = argv.indexOf(name);
@@ -18,6 +18,14 @@ export function runImportCli(argv) {
18
18
  const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
19
19
  if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep import: vault not found: ${vaultBase}\n`); process.exit(2); }
20
20
 
21
+ // Repair mode: backfill session_id into existing notes from the registry, then stop.
22
+ if (argv.includes('--stamp-ids')) {
23
+ const r = stampSessionIds(vaultBase);
24
+ if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(r, null, 2)}\n`); process.exit(0); }
25
+ process.stdout.write(`session_id carimbado em ${r.stamped} nota(s) · ${r.alreadyOk} já ok · ${r.missingFile} sem arquivo (de ${r.total} no registry)\n`);
26
+ process.exit(0);
27
+ }
28
+
21
29
  const projectRaw = opt(argv, '--project') || process.cwd();
22
30
  const projectPath = isAbsolute(projectRaw) ? projectRaw : resolve(process.cwd(), projectRaw);
23
31
  const source = (opt(argv, '--source') || 'all').toLowerCase();
@@ -4,8 +4,11 @@
4
4
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
- function skill(name, description, body) {
8
- return { name, description, body: `---\nname: ${name}\ndescription: ${description}\n---\n${body}` };
7
+ // A skill is a SKILL.md plus optional bundled files (templates/prompts) that ship in the same
8
+ // folder and are delivered together by `wendkeep sync-defs` (cpSync of the whole dir). The
9
+ // SKILL.md references them; the model reads them on demand — depth without bloating SKILL.md.
10
+ function skill(name, description, body, files = []) {
11
+ return { name, description, body: `---\nname: ${name}\ndescription: ${description}\n---\n${body}`, files };
9
12
  }
10
13
 
11
14
  const WORKFLOW = `# Loop a2 — o ciclo de trabalho do wendkeep
@@ -144,6 +147,10 @@ precisar de design" é onde suposições não-checadas mais custam. O design pod
144
147
  mas tem que existir e ser aprovado.
145
148
 
146
149
  Ao aprovar, o próximo passo é **wk-planning** (design → plano). Não pule pra implementação.
150
+
151
+ ## Template
152
+ Use o \`design-template.md\` (nesta pasta da skill) pra estruturar o design (contexto, abordagens,
153
+ decisões assinadas, tabela out-of-scope, aceite).
147
154
  `;
148
155
 
149
156
  const PLANNING = `# Planejamento — design vira plano de tarefas
@@ -174,6 +181,9 @@ de 2–5 min:
174
181
  "tratar erros apropriadamente", "similar à Task N".
175
182
  - DRY, YAGNI. Corte features que o design não pediu.
176
183
  - Nomes/assinaturas consistentes entre tarefas (uma função é \`x()\` em toda parte).
184
+
185
+ ## Template
186
+ Comece do \`plan-template.md\` (nesta pasta da skill) — a estrutura de arquivos + tarefas TDD.
177
187
  `;
178
188
 
179
189
  const VERIFY = `# Verificação independente — o passe fresco
@@ -201,6 +211,10 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
201
211
  - \`ok: false\` se algum requisito não tem cobertura que discrimina. Gap não é "quase lá" — é vermelho.
202
212
  - Não conserte aqui. Gap vira tarefa de correção na change; re-verifica depois.
203
213
  - O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
214
+
215
+ ## Templates (nesta pasta)
216
+ - \`spec-reviewer-prompt.md\` — cole ao spawnar o subagente verificador (read-only, autor≠verificador).
217
+ - \`verdict-template.json\` — o formato exato do \`verdict.json\` a gravar.
204
218
  `;
205
219
 
206
220
  const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
@@ -291,6 +305,10 @@ List explicitly what the change does **not** do. Undeclared scope becomes creep.
291
305
  ## Hard gate
292
306
  No code / scaffold / implementation action until a design is presented and approved. Then go to
293
307
  **wk-planning**.
308
+
309
+ ## Template
310
+ Use \`design-template.md\` (in this skill folder) to structure the design (context, approaches,
311
+ signed-off decisions, out-of-scope table, acceptance).
294
312
  `;
295
313
 
296
314
  const PLANNING_EN = `# Planning — design into a task plan
@@ -309,6 +327,9 @@ implementation (show code) → run and see it pass → checkpoint: suite green.
309
327
  ## Rules
310
328
  - Exact file paths, always. Real code in each step — no "TODO", "handle errors appropriately",
311
329
  "similar to Task N". DRY, YAGNI. Consistent names/signatures across tasks.
330
+
331
+ ## Template
332
+ Start from \`plan-template.md\` (in this skill folder) — the file map + TDD task structure.
312
333
  `;
313
334
 
314
335
  const VERIFY_EN = `# Independent verification — the fresh pass
@@ -329,24 +350,190 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
329
350
  - \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
330
351
  - Don't fix here — a gap becomes a fix task; re-verify after.
331
352
  - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
353
+
354
+ ## Templates (in this folder)
355
+ - \`spec-reviewer-prompt.md\` — hand it to the verifier sub-agent you spawn (read-only, author≠verifier).
356
+ - \`verdict-template.json\` — the exact shape of the \`verdict.json\` to write.
357
+ `;
358
+
359
+ // --- bundled templates (shipped alongside the relevant SKILL.md) -------------
360
+
361
+ // Shared, language-neutral verdict skeleton for the independent verify pass.
362
+ const VERDICT_TEMPLATE = `{
363
+ "slug": "<change-slug>",
364
+ "ok": true,
365
+ "coverage": [
366
+ { "req": "GATE-1", "covered": true, "evidence": "tests/foo.test.mjs:42" }
367
+ ],
368
+ "tasksHash": "<copie de verificacao.json — selo de frescor / copy from verificacao.json — freshness seal>",
369
+ "notes": []
370
+ }
371
+ `;
372
+
373
+ const REVIEWER_PROMPT_PT = `# Prompt — passe de verificação independente (read-only)
374
+
375
+ Entregue este prompt ao spawnar o subagente verificador (via o harness nativo — Task/Agent no
376
+ Claude). Ele NÃO é o autor: entra fresco, read-only, não edita nada.
377
+
378
+ ---
379
+ Você é o verificador independente de uma mudança do wendkeep. Não escreveu este código —
380
+ entre como se nunca o tivesse visto. Read-only.
381
+
382
+ Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência) e os
383
+ specs vivos em \`07-Specs/<capability>.md\`.
384
+
385
+ Para cada \`[req:ID]\` da mudança:
386
+ 1. Leia o **critério de aceite do requisito** no spec — NÃO leia a implementação primeiro.
387
+ 2. Ache o teste que cobre esse comportamento. Ele **discrimina**? (falharia sob uma
388
+ implementação errada; afirma valor/estado persistido, não "o mock foi chamado").
389
+ 3. Evidência \`arquivo:linha\`. Sem teste que discrimina = \`covered: false\` (é vermelho, não "quase").
390
+ 4. Cheque o resultado observável contra o critério — não contra o código.
391
+
392
+ Grave \`08-Mudanças/<slug>/verdict.json\` no formato de \`verdict-template.json\`. \`ok: false\` se
393
+ qualquer \`[req:]\` não tem cobertura que discrimina. Não conserte aqui — gap vira tarefa de
394
+ correção. O \`tasksHash\` vem do pacote (selo de frescor; alterou tarefa depois, o gate rejeita).
395
+ ---
396
+ `;
397
+
398
+ const REVIEWER_PROMPT_EN = `# Prompt — independent verification pass (read-only)
399
+
400
+ Hand this to the verifier sub-agent you spawn (via the native harness — Task/Agent on Claude).
401
+ It is NOT the author: fresh context, read-only, edits nothing.
402
+
403
+ ---
404
+ You are the independent verifier of a wendkeep change. You did not write this code — enter as if
405
+ you'd never seen it. Read-only.
406
+
407
+ Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence) and the
408
+ living specs in \`07-Specs/<capability>.md\`.
409
+
410
+ For each \`[req:ID]\`:
411
+ 1. Read the requirement's **acceptance criterion** in the spec — do NOT read the implementation first.
412
+ 2. Find the test covering that behaviour. Does it **discriminate**? (would fail under a wrong
413
+ implementation; asserts a persisted value/state, not "the mock was called").
414
+ 3. \`file:line\` evidence. No discriminating test = \`covered: false\` (red, not "almost").
415
+ 4. Check the observable result against the criterion — not the code.
416
+
417
+ Write \`08-Changes/<slug>/verdict.json\` in the shape of \`verdict-template.json\`. \`ok: false\` if any
418
+ \`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`
419
+ comes from the package (freshness seal; edit a task later and the gate rejects it as stale).
420
+ ---
421
+ `;
422
+
423
+ const PLAN_TEMPLATE_PT = `# Template — plano de tarefas (TDD, bite-sized)
424
+
425
+ ## Arquivos
426
+ - Criar: \`caminho/exato.mjs\`
427
+ - Modificar: \`caminho/existente.mjs:120-140\`
428
+ - Teste: \`tests/exato.test.mjs\`
429
+
430
+ ## Tarefa N — <nome>
431
+ - **Consome:** <o que usa de tarefas anteriores — assinaturas exatas>
432
+ - **Produz:** <o que tarefas seguintes usam — nomes/tipos exatos>
433
+
434
+ - [ ] N.1 escreva o teste que falha (mostre o código do teste) \`[req:<ID>]\`
435
+ - [ ] N.2 rode e veja falhar — pelo motivo certo (comando exato + saída esperada)
436
+ - [ ] N.3 implementação mínima (mostre o código) \`[sensor:<id>]\` se precisa de prova
437
+ - [ ] N.4 rode e veja passar
438
+ - [ ] N.5 checkpoint: suíte verde · commit
439
+
440
+ ## Regras
441
+ Caminhos exatos sempre. Código real em cada passo — nada de "TODO" / "tratar erros
442
+ apropriadamente" / "similar à Tarefa N". DRY, YAGNI. Nomes e assinaturas consistentes entre
443
+ tarefas (uma função é \`x()\` em toda parte). Cada tarefa termina num entregável testável sozinho.
444
+ `;
445
+
446
+ const PLAN_TEMPLATE_EN = `# Template — task plan (TDD, bite-sized)
447
+
448
+ ## Files
449
+ - Create: \`exact/path.mjs\`
450
+ - Modify: \`exact/existing.mjs:120-140\`
451
+ - Test: \`tests/exact.test.mjs\`
452
+
453
+ ## Task N — <name>
454
+ - **Consumes:** <what it uses from earlier tasks — exact signatures>
455
+ - **Produces:** <what later tasks rely on — exact names/types>
456
+
457
+ - [ ] N.1 write the failing test (show the test code) \`[req:<ID>]\`
458
+ - [ ] N.2 run and see it fail — for the right reason (exact command + expected output)
459
+ - [ ] N.3 minimal implementation (show the code) \`[sensor:<id>]\` if it needs proof
460
+ - [ ] N.4 run and see it pass
461
+ - [ ] N.5 checkpoint: suite green · commit
462
+
463
+ ## Rules
464
+ Exact paths always. Real code in every step — no "TODO" / "handle errors appropriately" /
465
+ "similar to Task N". DRY, YAGNI. Consistent names/signatures across tasks. Each task ends in an
466
+ independently testable deliverable.
467
+ `;
468
+
469
+ const DESIGN_TEMPLATE_PT = `# Template — documento de design
470
+
471
+ ## Contexto
472
+ <o problema, o estado atual, por que agora>
473
+
474
+ ## Abordagens consideradas
475
+ 1. **<A>** — <trade-off>.
476
+ 2. **<B>** — <trade-off>.
477
+ Recomendada: **<qual>** — <por quê>.
478
+
479
+ ## Design
480
+ <arquitetura, componentes, fluxo de dados, tratamento de erro, estratégia de teste — em seções
481
+ escaladas ao tamanho do problema>
482
+
483
+ ## Decisões e assumptions assinadas
484
+ - Assumo **X** porque **Y** — corrija se errado.
485
+
486
+ ## Out-of-scope (o contrato do que NÃO muda)
487
+ | Item | Por que fora |
488
+ |---|---|
489
+ | <x> | <razão> |
490
+
491
+ ## Aceite
492
+ <critérios verificáveis: para cada requisito, o teste que falha → passa>
493
+ `;
494
+
495
+ const DESIGN_TEMPLATE_EN = `# Template — design document
496
+
497
+ ## Context
498
+ <the problem, the current state, why now>
499
+
500
+ ## Approaches considered
501
+ 1. **<A>** — <trade-off>.
502
+ 2. **<B>** — <trade-off>.
503
+ Recommended: **<which>** — <why>.
504
+
505
+ ## Design
506
+ <architecture, components, data flow, error handling, test strategy — sections scaled to the
507
+ size of the problem>
508
+
509
+ ## Decisions and signed-off assumptions
510
+ - Assuming **X** because **Y** — correct me if wrong.
511
+
512
+ ## Out-of-scope (the contract of what does NOT change)
513
+ | Item | Why out |
514
+ |---|---|
515
+ | <x> | <reason> |
516
+
517
+ ## Acceptance
518
+ <verifiable criteria: for each requirement, the test that fails → passes>
332
519
  `;
333
520
 
334
521
  const WK_SKILLS_PT = [
335
522
  skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
336
523
  skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
337
524
  skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
338
- skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING),
339
- skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING),
340
- skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
525
+ skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_PT }]),
526
+ skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_PT }]),
527
+ skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_PT }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
341
528
  ];
342
529
 
343
530
  const WK_SKILLS_EN = [
344
531
  skill('wk-workflow', 'Use when starting any non-trivial change — orchestrates the a2 loop (explore, propose, apply, verify, archive) over the wendkeep commands.', WORKFLOW_EN),
345
532
  skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
346
533
  skill('wk-debugging', 'Use when something fails or breaks — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
347
- skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN),
348
- skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN),
349
- skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN),
534
+ skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN, [{ name: 'design-template.md', content: DESIGN_TEMPLATE_EN }]),
535
+ skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN, [{ name: 'plan-template.md', content: PLAN_TEMPLATE_EN }]),
536
+ skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN, [{ name: 'spec-reviewer-prompt.md', content: REVIEWER_PROMPT_EN }, { name: 'verdict-template.json', content: VERDICT_TEMPLATE }]),
350
537
  ];
351
538
 
352
539
  // Skill set for a locale. WK_SKILLS stays the pt-BR set for back-compat.
@@ -355,17 +542,23 @@ export function wkSkills(localeId = 'pt-BR') {
355
542
  }
356
543
  export const WK_SKILLS = WK_SKILLS_PT;
357
544
 
358
- // Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
545
+ // Seed each skill into <brainDir>/skills/<name>/ if absent (non-destructive): SKILL.md plus any
546
+ // bundled template/prompt files. Existing files are never overwritten, so re-seeding an older
547
+ // install just fills in the new template files alongside its SKILL.md.
359
548
  export function seedWkSkills(brainDir, localeId = 'pt-BR') {
360
549
  const created = [];
361
550
  for (const s of wkSkills(localeId)) {
362
551
  const dir = join(brainDir, 'skills', s.name);
363
552
  mkdirSync(dir, { recursive: true });
364
- const f = join(dir, 'SKILL.md');
365
- if (!existsSync(f)) {
366
- writeFileSync(f, s.body, 'utf8');
367
- created.push(f);
368
- }
553
+ const writeIfAbsent = (name, content) => {
554
+ const f = join(dir, name);
555
+ if (!existsSync(f)) {
556
+ writeFileSync(f, content, 'utf8');
557
+ created.push(f);
558
+ }
559
+ };
560
+ writeIfAbsent('SKILL.md', s.body);
561
+ for (const file of s.files || []) writeIfAbsent(file.name, file.content);
369
562
  }
370
563
  return created;
371
564
  }
package/src/taxonomy.mjs CHANGED
@@ -78,6 +78,10 @@ export const MCP_SERVER_KEY = 'wendkeep-vault';
78
78
  // installed package is the single source of truth (update with `npm update wendkeep`,
79
79
  // no re-copying). Returned as a spec the merge logic folds into settings.json.
80
80
  export const SESSION_HOOKS = [
81
+ // Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
82
+ // session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
83
+ // matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
84
+ { event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 15, order: -10, statusMessage: 'wendkeep: injecting memory + active change' },
81
85
  { event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, statusMessage: 'wendkeep: opening Obsidian session' },
82
86
  { event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, statusMessage: 'wendkeep: writing session checkpoint' },
83
87
  { event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, statusMessage: 'wendkeep: ensuring active session' },