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 +22 -0
- package/README.md +2 -0
- package/hooks/lessons-core.mjs +10 -1
- package/hooks/spec-core.mjs +12 -1
- package/package.json +1 -1
- package/src/change.mjs +19 -7
- package/src/skills-seed.mjs +3 -1
- package/src/verify.mjs +29 -5
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
|
+

|
|
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.
|
package/hooks/lessons-core.mjs
CHANGED
|
@@ -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
|
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
75
|
-
try {
|
|
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)
|
|
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 });
|
package/src/skills-seed.mjs
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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');
|