wendkeep 0.20.0 → 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,36 @@ 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
+
28
+ ## [0.20.1] — 2026-07-06
29
+
30
+ ### Changed
31
+ - The interactive (and text-fallback) companion picker in `wendkeep init` **no longer lists
32
+ dotcontext**. The native a2 loop replaces it, so leaving it in the prompt was just clutter. It
33
+ stays reachable for anyone already invested via an explicit `--companions dotcontext` — the
34
+ hiding is UI-only (`resolveCompanions` still honors the id). New `selectableCompanions()` helper
35
+ drives the picker.
36
+
7
37
  ## [0.20.0] — 2026-07-06
8
38
 
9
39
  Richer skills: bundled templates (multi-file).
@@ -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.0",
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);
package/src/init.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  mcpServerEntry,
14
14
  hookCommand,
15
15
  deriveVaultDirName,
16
- COMPANIONS,
16
+ selectableCompanions,
17
17
  resolveCompanions,
18
18
  companionSettingsPatch,
19
19
  companionMcpPatch,
@@ -260,11 +260,11 @@ export async function runInit(argv) {
260
260
  } else if (process.stdin.isTTY && !args.yes) {
261
261
  if (canInteractiveSelect()) {
262
262
  log(''); // the checkbox menu renders its own header
263
- companions = await selectCompanionsInteractive(COMPANIONS, { labels: P.menu });
263
+ companions = await selectCompanionsInteractive(selectableCompanions(), { labels: P.menu });
264
264
  } else {
265
265
  // Text fallback (no raw-mode TTY): list + comma input with clear instructions.
266
266
  log(P.companionsHeader);
267
- for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
267
+ for (const c of selectableCompanions()) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
268
268
  const def = resolveCompanions({}).join(',');
269
269
  const rl = createInterface({ input: process.stdin, output: process.stdout });
270
270
  const ans = (await rl.question(P.companionsAsk(def))).trim();
package/src/taxonomy.mjs CHANGED
@@ -134,6 +134,9 @@ export const COMPANIONS = [
134
134
  // Kept selectable for anyone already invested in it; not a default.
135
135
  label: 'dotcontext — legado (o loop a2 nativo do wendkeep substitui; não recomendado)',
136
136
  default: false,
137
+ // Hidden from the interactive/text companion picker — the native a2 loop replaces it.
138
+ // Still reachable for anyone already invested via explicit `--companions dotcontext`.
139
+ hidden: true,
137
140
  // MCP-only (no Claude Code plugin). Agent-agnostic server, @latest surface.
138
141
  mcp: { key: 'dotcontext', entry: { type: 'stdio', command: 'npx', args: ['-y', '@dotcontext/mcp@latest'], env: {} } },
139
142
  // Lifecycle hooks via the pinned CLI; wired by wendkeep (single writer).
@@ -145,6 +148,12 @@ export const COMPANIONS = [
145
148
 
146
149
  const COMPANION_BY_ID = Object.fromEntries(COMPANIONS.map((c) => [c.id, c]));
147
150
 
151
+ // Companions offered in the interactive / text picker. Excludes `hidden` ones (e.g. the legacy
152
+ // dotcontext) — those stay reachable only via an explicit `--companions <id>`.
153
+ export function selectableCompanions() {
154
+ return COMPANIONS.filter((c) => !c.hidden);
155
+ }
156
+
148
157
  // Resolve the companion ids for the NON-interactive path (the interactive prompt
149
158
  // supplies its own selection). --no-companions wins; an explicit flag selects the
150
159
  // named ids (unknown dropped) in registry order; otherwise the defaults.