wendkeep 0.20.1 → 0.21.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,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.21.0] — 2026-07-08
8
+
9
+ Process enforcement — fixes from a real planning failure (production session): the model planned
10
+ in chat, never invoked the wk-* skills, left the change scaffold raw and archived it with
11
+ `--force`, minting a bogus ADR.
12
+
13
+ ### Added
14
+ - **`<wk_process>` router injected every session** (brain-inject): the enforcement layer the
15
+ skills were missing. Plan → wk-brainstorming + wk-planning; record → `change new` + FILL
16
+ proposta/design/tarefas; implement → wk-tdd; close → verify + wk-verify + archive. States
17
+ explicitly that `archive --force` is the user's call, never the agent's. Localized (pt-BR/en).
18
+ - **G0 — anti-scaffold gate**: `change archive` now blocks when proposta/design/tarefas still
19
+ carry the scaffold placeholders (`(motivo da mudança)`, `(abordagem técnica)`,
20
+ `(primeira tarefa)` + en variants) — an unfilled scaffold is not a completed change.
21
+ `--force` still escapes (human hatch); new `scaffoldPlaceholders(dir)` in change-core.
22
+
23
+ ### Fixed
24
+ - **session-ensure now stamps `session_id`** in the notes it creates — the 4th note-creation
25
+ path, missed in 0.18.0 (it has its own skeleton builder). Notes born from UserPromptSubmit
26
+ (no SessionStart, e.g. resumed windows) were coming out without identity.
27
+
7
28
  ## [0.20.1] — 2026-07-06
8
29
 
9
30
  ### Changed
@@ -9,6 +9,36 @@ import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.
9
9
  import { brainDir } from './brain-core.mjs';
10
10
  import { buildActiveChangeInjection } from './change-core.mjs';
11
11
  import { buildLessonsInjection } from './lessons-core.mjs';
12
+ import { getLocale } from './locale.mjs';
13
+
14
+ // The process ROUTER — the enforcement layer. The wk-* skills are passive files; without a
15
+ // standing instruction the model plans in chat, leaves the change scaffold raw and forces the
16
+ // gate (seen in production: change archived with `(primeira tarefa)` open, via --force). This
17
+ // block is injected EVERY session so planning always routes through the a2 loop.
18
+ function processRouter(localeId) {
19
+ if (localeId === 'en') {
20
+ return [
21
+ '<wk_process>',
22
+ 'Spec-driven process (mandatory for any non-trivial task):',
23
+ '1. Plan: skill wk-brainstorming (approved design) → wk-planning (task plan).',
24
+ '2. Record: `wendkeep change new <slug>` and FILL 08-Changes/<slug>/ — proposta.md (why/what), design.md (the approved approach), tarefas.md (the plan\'s tasks, `- [ ] N.N` with [req:ID]/[sensor:id]). Never leave the scaffold placeholders.',
25
+ '3. Implement: wk-tdd per task; tick `- [x]` as you finish. Something broke? wk-debugging.',
26
+ '4. Close: `wendkeep verify` (+ `--deep` + skill wk-verify) → `wendkeep change archive`.',
27
+ 'NEVER `archive --force` on your own — a red gate means pending work; --force is the user\'s call, not yours.',
28
+ '</wk_process>',
29
+ ].join('\n');
30
+ }
31
+ return [
32
+ '<wk_process>',
33
+ 'Processo spec-driven (obrigatório em tarefa não-trivial):',
34
+ '1. Planejar: skill wk-brainstorming (design aprovado) → wk-planning (plano de tarefas).',
35
+ '2. Registrar: `wendkeep change new <slug>` e PREENCHA 08-Mudanças/<slug>/ — proposta.md (porquê/o quê), design.md (a abordagem aprovada), tarefas.md (as tarefas do plano, `- [ ] N.N` com [req:ID]/[sensor:id]). Nunca deixe os placeholders do scaffold.',
36
+ '3. Implementar: wk-tdd por tarefa; marque `- [x]` ao concluir. Quebrou algo? wk-debugging.',
37
+ '4. Fechar: `wendkeep verify` (+ `--deep` + skill wk-verify) → `wendkeep change archive`.',
38
+ 'PROIBIDO `archive --force` por conta própria — gate vermelho significa trabalho pendente; --force é decisão do usuário, não sua.',
39
+ '</wk_process>',
40
+ ].join('\n');
41
+ }
12
42
 
13
43
  const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
14
44
 
@@ -26,9 +56,10 @@ export function buildInjection(vaultBase) {
26
56
  lines.push('*…truncado pelo budget — fonte completa: .brain/CORE.md + .brain/DIGEST.md*');
27
57
  }
28
58
  const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
59
+ const router = processRouter(getLocale(vaultBase).id);
29
60
  const change = buildActiveChangeInjection(vaultBase);
30
61
  const lessons = buildLessonsInjection(vaultBase);
31
- return [brain, change, lessons].filter(Boolean).join('\n');
62
+ return [brain, router, change, lessons].filter(Boolean).join('\n');
32
63
  }
33
64
 
34
65
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -53,6 +53,25 @@ ${en ? '(behaviour / scenarios)' : '(comportamento / cenários)'}
53
53
  return { proposta, design, tarefas, specDelta };
54
54
  }
55
55
 
56
+ // Scaffold placeholder markers per file (pt + en, mirrors renderChangeScaffold). A change whose
57
+ // proposta/design/tarefas still carry these was never actually planned — archiving it mints a
58
+ // bogus ADR and pollutes _arquivo (seen in production). The archive gate blocks on them.
59
+ const SCAFFOLD_MARKERS = [
60
+ ['proposta.md', ['(motivo da mudança)', '(escopo da mudança)', '(reason for the change)', '(scope of the change)']],
61
+ ['design.md', ['(abordagem técnica)', '(technical approach)']],
62
+ ['tarefas.md', ['(primeira tarefa)', '(first task)']],
63
+ ];
64
+
65
+ export function scaffoldPlaceholders(dir) {
66
+ const found = [];
67
+ for (const [file, markers] of SCAFFOLD_MARKERS) {
68
+ let content = '';
69
+ try { content = readFileSync(join(dir, file), 'utf8'); } catch { continue; }
70
+ for (const m of markers) if (content.includes(m)) found.push(`${file}: ${m}`);
71
+ }
72
+ return found;
73
+ }
74
+
56
75
  export function activeChange(vaultBase) {
57
76
  try {
58
77
  const m = readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m);
@@ -35,7 +35,7 @@ function sessionIdFromInput(input) {
35
35
  return input.session_id || input.sessionId || input.codex_session_id || '';
36
36
  }
37
37
 
38
- function buildSessionContent({ relPath, now, summary = 'session', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
38
+ function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
39
39
  const date = formatDate(now);
40
40
  const startedAt = formatLocalIso(now);
41
41
  const titleTime = formatTime(now).slice(0, 5);
@@ -48,6 +48,7 @@ date: ${date}
48
48
  started_at: ${startedAt}
49
49
  ended_at:
50
50
  provider: ${provider.id}
51
+ session_id: ${sessionId ? yamlQuote(sessionId) : ''}
51
52
  status: active
52
53
  summary: ${yamlQuote(summary)}
53
54
  cssclasses:
@@ -267,7 +268,7 @@ function createSession({ vaultBase, sessionId, input, now }) {
267
268
  const summary = sessionSummaryFromInput(input);
268
269
  const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
269
270
  const startedAt = formatLocalIso(now);
270
- writeFileSync(absPath, buildSessionContent({ relPath, now, summary }), 'utf-8');
271
+ writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId }), 'utf-8');
271
272
  writeControl(vaultBase, {
272
273
  status: 'active',
273
274
  session_file: relPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.20.1",
3
+ "version": "0.21.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/change.mjs CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  parseTasks,
9
9
  setTaskDone,
10
10
  archiveChange,
11
+ scaffoldPlaceholders,
11
12
  } from '../hooks/change-core.mjs';
12
13
  import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
13
14
  import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
@@ -154,6 +155,13 @@ export function runChange(argv) {
154
155
  if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
155
156
  // Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
156
157
  const gate = (dir) => {
158
+ // G0: um scaffold nunca preenchido não é uma mudança concluída — arquivar geraria um
159
+ // ADR falso. Bloqueia antes de qualquer outro check (o --force ainda escapa, mas a
160
+ // regra injetada proíbe o agente de usá-lo sem o usuário pedir).
161
+ const placeholders = scaffoldPlaceholders(dir);
162
+ if (placeholders.length && !rest.includes('--force')) {
163
+ return { ok: false, failing: [`scaffold não preenchido (${placeholders.join('; ')}) — preencha proposta/design/tarefas com o design e o plano antes de arquivar`] };
164
+ }
157
165
  let tarefasMd = '';
158
166
  try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); } catch { /* no tasks */ }
159
167
  const tasks = parseTasks(tarefasMd);