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 +21 -0
- package/hooks/brain-inject.mjs +32 -1
- package/hooks/change-core.mjs +19 -0
- package/hooks/session-ensure.mjs +3 -2
- package/package.json +1 -1
- package/src/change.mjs +8 -0
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
|
package/hooks/brain-inject.mjs
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) {
|
package/hooks/change-core.mjs
CHANGED
|
@@ -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);
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -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.
|
|
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);
|