wendkeep 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.6.1] — 2026-07-05
8
+
9
+ Hardening: CI + real-world gate holes found by self-audit.
10
+
11
+ ### Added
12
+ - **CI (GitHub Actions):** test + check matrix on ubuntu/windows × Node 18/20/22.
13
+ - **Open-task gate:** `change archive` blocks while tasks are open (`- [ ]`, including mutation
14
+ fix-tasks `M.n` — a surviving mutant can no longer be archived). Explicit escape: `--force`.
15
+ - **Freshness seal (`tasksHash`):** `verify --deep` fingerprints `tarefas.md` into the package
16
+ and verdict; the gate rejects a verdict minted against different tasks as stale. Pre-0.6.1
17
+ verdicts (no hash) still accepted.
18
+ - **Auto-lesson on mutation escalation:** the 3rd surviving round records a project-local lesson.
19
+ - **Session link in proposta:** `change new` fills `source:` with the active session (graph edge
20
+ proposta → sessão).
21
+
22
+ ### Fixed
23
+ - `.mutation-round` now resets when the report comes back clean (a future survivor starts a
24
+ fresh 3-round cycle instead of instantly escalating).
25
+ - `verify` exits 1 when mutants survive (was 0 — CI couldn't see it).
26
+ - `.brain/lessons/` capped at 50 (oldest pruned) instead of growing unbounded.
27
+
7
28
  ## [0.6.0] — 2026-07-05
8
29
 
9
30
  Enforcement layer (Wave B of the TLC-parity program) — closes TLC parity.
@@ -120,6 +141,7 @@ Initial release — the capture engine, extracted from a system in daily product
120
141
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
121
142
 
122
143
  <!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
144
+ [0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
123
145
  [0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
124
146
  [0.5.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.5.0
125
147
  [0.4.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.4.0
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # wendkeep
2
2
 
3
+ ![test](https://github.com/rogersialves/wendkeep/actions/workflows/test.yml/badge.svg)
4
+
3
5
  **A persistent‑memory harness for AI coding agents, built on your Obsidian vault.** Every Claude Code / Codex session is captured turn‑by‑turn into local Markdown — with token/cost tracking, auto‑extracted decisions, bugs and learnings, and a curated memory layer injected back at the start of the next session. On top of that memory core sits a native, zero‑dependency **change lifecycle** (spec → change → TDD → sensor‑gated archive) that keeps intent, work and proof wikilinked in one graph. 100% local, open‑core.
4
6
 
5
7
  > Status: `v0.1` — extracted from a system in daily production use. The capture engine, cost tracking and graph wiring are battle‑tested; the cross‑platform installer (`wendkeep init`) is the new part. See [`docs/`](docs/) for the project's strategy and decision log.
@@ -1,9 +1,11 @@
1
1
  // hooks/lessons-core.mjs — project-local lessons from verification failures (Wave B).
2
2
  // A failure distilled into a terse lesson; brain-inject surfaces the recent ones at
3
3
  // SessionStart so the framework gets sharper on your codebase. No external deps.
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
+ const MAX_LESSONS = 50; // dir cap (#7): oldest (filename asc = date asc) pruned first
8
+
7
9
  function slugify(s) {
8
10
  return String(s)
9
11
  .toLowerCase()
@@ -22,6 +24,13 @@ export function addLesson(vaultBase, { trigger, lesson, sourceChange = '', dateS
22
24
  `---\ntype: lesson\ntrigger: ${JSON.stringify(String(trigger))}\nsource: ${sourceChange}\ndate: ${dateStr}\n---\n\n${lesson}\n`,
23
25
  'utf8',
24
26
  );
27
+ // Cap the directory (#7): prune oldest-by-name (date-prefixed = chronological).
28
+ try {
29
+ const files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
30
+ for (const f of files.slice(0, Math.max(0, files.length - MAX_LESSONS))) {
31
+ unlinkSync(join(dir, f));
32
+ }
33
+ } catch { /* prune é bônus */ }
25
34
  return path;
26
35
  }
27
36
 
@@ -1,9 +1,15 @@
1
1
  // hooks/spec-core.mjs — living spec (07-Specs) + change delta merge (OpenSpec native).
2
2
  // Pure parsing/merge + promoteSpecs (fs). No import from change-core (avoids a cycle).
3
+ import { createHash } from 'node:crypto';
3
4
  import { readFileSync, writeFileSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { ensureDir } from './obsidian-common.mjs';
6
7
 
8
+ // Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
9
+ export function tasksHashOf(md) {
10
+ return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
11
+ }
12
+
7
13
  const SPECS_DIR = '07-Specs';
8
14
  const REQ_RE = /^### Requisito:\s*(.+)$/gm;
9
15
 
@@ -104,10 +110,15 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
104
110
  // Gate check for the independent verdict (Wave A). A requirement-bearing change must have
105
111
  // a verdict that is ok and covers every declared req id. A requirement-less change passes:
106
112
  // nothing for an independent verifier to check — the sensor gate is already the proof.
107
- export function evaluateVerdict(verdict, reqIds) {
113
+ export function evaluateVerdict(verdict, reqIds, { tasksHash } = {}) {
108
114
  const ids = reqIds || [];
109
115
  if (ids.length === 0) return { ok: true, missing: [] };
110
116
  if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
117
+ // Freshness (G3/#6): a verdict minted against a different tarefas.md is stale. Verdicts
118
+ // without a hash (pre-0.6.1) are accepted for backward compat.
119
+ if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
120
+ return { ok: false, missing: [], stale: true };
121
+ }
111
122
  const covered = new Set((verdict.coverage || []).filter((c) => c.covered).map((c) => c.req));
112
123
  const missing = ids.filter((r) => !covered.has(r));
113
124
  return { ok: missing.length === 0, missing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/change.mjs CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  archiveChange,
10
10
  } from '../hooks/change-core.mjs';
11
11
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
12
- import { evaluateVerdict } from '../hooks/spec-core.mjs';
13
- import { getNextAdrNumber } from '../hooks/obsidian-common.mjs';
12
+ import { evaluateVerdict, tasksHashOf } from '../hooks/spec-core.mjs';
13
+ import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
14
14
 
15
15
  function resolveVault(argv) {
16
16
  let vault;
@@ -40,7 +40,10 @@ export function runChange(argv) {
40
40
  if (sub === 'new') {
41
41
  const slug = slugArg();
42
42
  if (!slug) { process.stderr.write('wendkeep change new: missing <slug>\n'); process.exit(2); }
43
- const r = newChange(vaultBase, slug, { dateStr: today(), simple: rest.includes('--simple') });
43
+ // G2: link the active session into the proposta's source: (graph edge proposta->sessão).
44
+ let sessionRel = '';
45
+ try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* sem control */ }
46
+ const r = newChange(vaultBase, slug, { dateStr: today(), simple: rest.includes('--simple'), sessionRel });
44
47
  process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
45
48
  process.exit(0);
46
49
  }
@@ -71,8 +74,14 @@ export function runChange(argv) {
71
74
  if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
72
75
  // Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
73
76
  const gate = (dir) => {
74
- let tasks = [];
75
- try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* no tasks */ }
77
+ let tarefasMd = '';
78
+ try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); } catch { /* no tasks */ }
79
+ const tasks = parseTasks(tarefasMd);
80
+ // G1: uma change não arquiva com tarefa aberta (inclui fix-tasks M.n de mutação).
81
+ const open = tasks.filter((t) => !t.done);
82
+ if (open.length && !rest.includes('--force')) {
83
+ return { ok: false, failing: [`${open.length} tarefa(s) aberta(s) (ex.: ${open[0].id} ${open[0].text}) — conclua ou use --force`] };
84
+ }
76
85
  const required = requiredSensors(tasks);
77
86
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
78
87
  let evidence = [];
@@ -82,8 +91,11 @@ export function runChange(argv) {
82
91
  // Independent verdict (Wave A): required only when the change declares [req:] tasks.
83
92
  let verdict = null;
84
93
  try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* none */ }
85
- const v = evaluateVerdict(verdict, reqIds);
86
- if (!v.ok) return { ok: false, failing: verdict ? [`verdict incompleto: falta ${v.missing.join(', ')}`] : ['sem verdict — rode `wendkeep verify --deep` + skill wk-verify'] };
94
+ const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
95
+ if (!v.ok) {
96
+ if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
97
+ return { ok: false, failing: verdict ? [`verdict incompleto: falta ${v.missing.join(', ')}`] : ['sem verdict — rode `wendkeep verify --deep` + skill wk-verify'] };
98
+ }
87
99
  return { ok: true, failing: [] };
88
100
  };
89
101
  const r = archiveChange(vaultBase, slug, { dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate });
@@ -190,7 +190,9 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
190
190
  passaria sob impl errada). Evidência \`arquivo:linha\`.
191
191
  3. Outcome check ancorado no spec: o resultado observável bate com o critério de aceite?
192
192
  4. Grave \`08-Mudanças/<slug>/verdict.json\`:
193
- \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "notes": [] }\`.
193
+ \`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "tasksHash": "<copie do verificacao.json>", "notes": [] }\`.
194
+ O \`tasksHash\` vem do pacote — é o selo de frescor; sem ele (ou com tarefas alteradas
195
+ depois), o gate rejeita o verdict como stale.
194
196
 
195
197
  ## Regras
196
198
 
package/src/verify.mjs CHANGED
@@ -1,10 +1,17 @@
1
1
  // `wendkeep verify [--change <slug>]` — run a change's task sensors, record evidence.
2
2
  // Sensors run at the PROJECT root (--project or cwd); the change + evidence live in
3
3
  // the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
4
- import { readFileSync, writeFileSync } from 'node:fs';
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
7
  import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
8
+ import { tasksHashOf } from '../hooks/spec-core.mjs';
9
+ import { addLesson } from '../hooks/lessons-core.mjs';
10
+
11
+ function today() {
12
+ const d = new Date();
13
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
14
+ }
8
15
 
9
16
  function opt(argv, name) {
10
17
  const i = argv.indexOf(name);
@@ -31,20 +38,35 @@ export function runVerify(argv) {
31
38
  const evidence = runSensors(sensors, ids, { cwd: projectRoot });
32
39
  writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
33
40
 
34
- // Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate.
41
+ // Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate. A surviving
42
+ // mutant always fails verify (exit 1): the suite does not discriminate yet. A clean report
43
+ // resets the round counter so a future survivor starts a fresh cycle.
35
44
  const withSurvivors = evidence.filter((e) => e.survivors && e.survivors.length);
36
- if (withSurvivors.length) {
37
- const roundFile = join(changeDir, '.mutation-round');
45
+ const roundFile = join(changeDir, '.mutation-round');
46
+ if (!withSurvivors.length) {
47
+ try { unlinkSync(roundFile); } catch { /* nunca houve rodada */ }
48
+ } else {
38
49
  let round = 0;
39
50
  try { round = Number(readFileSync(roundFile, 'utf8').trim()) || 0; } catch { /* first round */ }
40
51
  if (round >= 3) {
41
52
  process.stderr.write('verify: mutantes ainda sobrevivem após 3 rodadas — revise os testes à mão.\n');
53
+ const flat = withSurvivors.flatMap((e) => e.survivors.map((s) => `${s.file}:${s.line}`));
54
+ try {
55
+ addLesson(vaultBase, {
56
+ trigger: `mutantes persistentes em ${slug}`,
57
+ lesson: `3 rodadas de fix-tasks não mataram: ${flat.join(', ')} — os testes desses pontos não discriminam.`,
58
+ sourceChange: slug,
59
+ dateStr: today(),
60
+ });
61
+ } catch { /* lesson é bônus, nunca derruba o verify */ }
42
62
  } else {
43
63
  let added = 0;
44
64
  for (const e of withSurvivors) added += appendFixTasks(changeDir, e.survivors, e.id);
45
65
  writeFileSync(roundFile, String(round + 1), 'utf8');
46
66
  process.stdout.write(`verify: ${added} fix-task(s) de mutação (rodada ${round + 1}/3)\n`);
47
67
  }
68
+ process.stderr.write('verify: mutantes sobreviventes — a suíte não discrimina ainda.\n');
69
+ process.exit(1);
48
70
  }
49
71
 
50
72
  // Same rule as the archive gate: evidence carries severity, so evaluateGate blocks
@@ -61,15 +83,17 @@ export function runVerify(argv) {
61
83
  if (argv.includes('--deep')) {
62
84
  const tasks = parseTasks(tarefas);
63
85
  const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
86
+ const tasksHash = tasksHashOf(tarefas);
64
87
  const pkg = {
65
88
  slug,
89
+ tasksHash,
66
90
  requirements: reqIds.map((id) => ({ id })),
67
91
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
68
92
  sensors: evidence,
69
93
  };
70
94
  writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
71
95
  if (reqIds.length === 0) {
72
- writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
96
+ writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
73
97
  process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
74
98
  } else {
75
99
  process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');