wendkeep 0.6.1 → 0.8.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 +41 -0
- package/README.md +36 -3
- package/bin/wendkeep.mjs +14 -1
- package/hooks/brain-core.mjs +2 -1
- package/hooks/change-core.mjs +45 -34
- package/hooks/harness-doctor.mjs +4 -3
- package/hooks/linked-notes.mjs +8 -6
- package/hooks/locale.mjs +73 -0
- package/hooks/obsidian-common.mjs +12 -9
- package/hooks/session-ensure.mjs +1 -1
- package/hooks/session-start.mjs +1 -1
- package/hooks/session-stop.mjs +5 -3
- package/hooks/spec-core.mjs +10 -7
- package/hooks/vault-health.mjs +3 -1
- package/package.json +2 -1
- package/schema/wendkeep.sensors.schema.json +34 -0
- package/src/change.mjs +84 -4
- package/src/dotcontext-seed.mjs +1 -1
- package/src/init.mjs +24 -8
- package/src/sensors.mjs +23 -0
- package/src/spec.mjs +54 -0
- package/src/sync-defs.mjs +61 -2
- package/src/taxonomy.mjs +1 -0
- package/src/validate-core.mjs +34 -9
- package/src/vault-theme.mjs +40 -30
- package/src/verify.mjs +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"hooks",
|
|
14
|
+
"schema",
|
|
14
15
|
"README.md",
|
|
15
16
|
"CHANGELOG.md"
|
|
16
17
|
],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json",
|
|
4
|
+
"title": "wendkeep.sensors.json",
|
|
5
|
+
"description": "Native sensor config for the wendkeep harness (harness contract v1.1).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "sensors"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string" },
|
|
10
|
+
"version": { "const": 1 },
|
|
11
|
+
"source": { "type": "string" },
|
|
12
|
+
"sensors": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["id", "command"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"id": { "type": "string", "minLength": 1 },
|
|
19
|
+
"name": { "type": "string" },
|
|
20
|
+
"description": { "type": "string" },
|
|
21
|
+
"severity": { "enum": ["critical", "warning"], "default": "critical" },
|
|
22
|
+
"type": { "enum": ["command", "mutation"], "default": "command" },
|
|
23
|
+
"command": { "type": "string", "minLength": 1 },
|
|
24
|
+
"report": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "type: mutation only — path (project-relative) to the mutation-testing-elements report."
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"additionalProperties": false
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
}
|
package/src/change.mjs
CHANGED
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
activeChange,
|
|
7
7
|
listChanges,
|
|
8
8
|
parseTasks,
|
|
9
|
+
setTaskDone,
|
|
9
10
|
archiveChange,
|
|
10
11
|
} from '../hooks/change-core.mjs';
|
|
11
12
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
12
|
-
import { evaluateVerdict, tasksHashOf } from '../hooks/spec-core.mjs';
|
|
13
|
+
import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta } from '../hooks/spec-core.mjs';
|
|
13
14
|
import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
|
|
15
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
14
16
|
|
|
15
17
|
function resolveVault(argv) {
|
|
16
18
|
let vault;
|
|
@@ -27,6 +29,13 @@ function resolveVault(argv) {
|
|
|
27
29
|
return isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function opt(argv, name) {
|
|
33
|
+
const i = argv.indexOf(name);
|
|
34
|
+
if (i >= 0) return argv[i + 1];
|
|
35
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
36
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
function today() {
|
|
31
40
|
const d = new Date();
|
|
32
41
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
@@ -35,7 +44,8 @@ function today() {
|
|
|
35
44
|
export function runChange(argv) {
|
|
36
45
|
const [sub, ...rest] = argv;
|
|
37
46
|
const vaultBase = resolveVault(rest);
|
|
38
|
-
const
|
|
47
|
+
const VALUE_FLAGS = new Set(['--vault', '--change', '--project']);
|
|
48
|
+
const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
39
49
|
|
|
40
50
|
if (sub === 'new') {
|
|
41
51
|
const slug = slugArg();
|
|
@@ -60,7 +70,7 @@ export function runChange(argv) {
|
|
|
60
70
|
const slug = slugArg();
|
|
61
71
|
if (!slug) { process.stderr.write('wendkeep change show: missing <slug>\n'); process.exit(2); }
|
|
62
72
|
let md;
|
|
63
|
-
try { md = readFileSync(join(vaultBase,
|
|
73
|
+
try { md = readFileSync(join(vaultBase, getLocale(vaultBase).folders.changes, slug, 'tarefas.md'), 'utf8'); }
|
|
64
74
|
catch { process.stderr.write(`wendkeep change show: not found: ${slug}\n`); process.exit(2); }
|
|
65
75
|
const tasks = parseTasks(md);
|
|
66
76
|
const open = tasks.filter((t) => !t.done).length;
|
|
@@ -69,6 +79,76 @@ export function runChange(argv) {
|
|
|
69
79
|
process.exit(0);
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
if (sub === 'status') {
|
|
83
|
+
const slug = slugArg() || activeChange(vaultBase);
|
|
84
|
+
if (!slug) { process.stderr.write('wendkeep change status: no change (arg or active)\n'); process.exit(2); }
|
|
85
|
+
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
86
|
+
let tarefasMd;
|
|
87
|
+
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); }
|
|
88
|
+
catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
|
|
89
|
+
const tasks = parseTasks(tarefasMd);
|
|
90
|
+
const done = tasks.filter((t) => t.done).length;
|
|
91
|
+
process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase) ? ' (ativa)' : ''}\n`);
|
|
92
|
+
let specs = [];
|
|
93
|
+
try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); } catch { /* sem proposta */ }
|
|
94
|
+
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
95
|
+
process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
|
|
96
|
+
for (const t of tasks) {
|
|
97
|
+
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.req ? ` [req:${t.req}]` : ''}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
|
|
98
|
+
}
|
|
99
|
+
let evidence = null;
|
|
100
|
+
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
101
|
+
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
102
|
+
else process.stdout.write('evidencia: ausente\n');
|
|
103
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
104
|
+
let verdict = null;
|
|
105
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
106
|
+
if (!reqIds.length) process.stdout.write('verdict: não exigido (sem [req:])\n');
|
|
107
|
+
else if (!verdict) process.stdout.write('verdict: ausente — rode `wendkeep verify --deep` + wk-verify\n');
|
|
108
|
+
else {
|
|
109
|
+
const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
|
|
110
|
+
process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
|
|
111
|
+
}
|
|
112
|
+
try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (sub === 'done' || sub === 'undone') {
|
|
117
|
+
const taskId = slugArg();
|
|
118
|
+
if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
|
|
119
|
+
const slug = opt(rest, '--change') || activeChange(vaultBase);
|
|
120
|
+
if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
|
|
121
|
+
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
122
|
+
let ok = false;
|
|
123
|
+
try { ok = setTaskDone(dir, taskId, sub === 'done'); } catch { /* sem tarefas.md */ }
|
|
124
|
+
if (!ok) { process.stderr.write(`wendkeep change ${sub}: task não encontrada: ${taskId}\n`); process.exit(2); }
|
|
125
|
+
process.stdout.write(`task ${taskId}: ${sub === 'done' ? '[x]' : '[ ]'}\n`);
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (sub === 'diff') {
|
|
130
|
+
const slug = slugArg() || activeChange(vaultBase);
|
|
131
|
+
if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
|
|
132
|
+
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
133
|
+
let specs = [];
|
|
134
|
+
try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); }
|
|
135
|
+
catch { process.stderr.write(`wendkeep change diff: not found: ${slug}\n`); process.exit(2); }
|
|
136
|
+
if (!specs.length) { process.stdout.write('diff: sem specs declaradas na proposta\n'); process.exit(0); }
|
|
137
|
+
for (const cap of specs) {
|
|
138
|
+
let delta;
|
|
139
|
+
try { delta = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8')); }
|
|
140
|
+
catch { process.stdout.write(`! sem delta para ${cap}\n`); continue; }
|
|
141
|
+
process.stdout.write(`spec: ${cap}\n`);
|
|
142
|
+
for (const r of delta.added) process.stdout.write(` + ${r.id || r.name} (ADDED)\n`);
|
|
143
|
+
for (const r of delta.modified) process.stdout.write(` ~ ${r.id || r.name} (MODIFIED)\n`);
|
|
144
|
+
for (const k of delta.removed) process.stdout.write(` - ${k} (REMOVED)\n`);
|
|
145
|
+
let living = [];
|
|
146
|
+
try { living = parseRequirements(readFileSync(join(vaultBase, getLocale(vaultBase).folders.specs, `${cap}.md`), 'utf8')); } catch { /* nova capability */ }
|
|
147
|
+
for (const w of applyDelta(living, delta).warnings) process.stdout.write(` ! ${w}\n`);
|
|
148
|
+
}
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
72
152
|
if (sub === 'archive') {
|
|
73
153
|
const slug = slugArg() || activeChange(vaultBase);
|
|
74
154
|
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
@@ -109,6 +189,6 @@ export function runChange(argv) {
|
|
|
109
189
|
process.exit(0);
|
|
110
190
|
}
|
|
111
191
|
|
|
112
|
-
process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, archive.\n`);
|
|
192
|
+
process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, status, done, undone, diff, archive.\n`);
|
|
113
193
|
process.exit(2);
|
|
114
194
|
}
|
package/src/dotcontext-seed.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function renderSensorsJson(scripts = {}) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
return `${JSON.stringify({ version: 1, source: 'manual', sensors }, null, 2)}\n`;
|
|
40
|
+
return `${JSON.stringify({ $schema: 'https://raw.githubusercontent.com/rogersialves/wendkeep/main/schema/wendkeep.sensors.schema.json', version: 1, source: 'manual', sensors }, null, 2)}\n`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function readProjectScripts(projectPath) {
|
package/src/init.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
34
34
|
import { seedDefinitions, syncDefs } from './sync-defs.mjs';
|
|
35
35
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
36
|
+
import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
|
|
36
37
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
37
38
|
|
|
38
39
|
function parseArgs(argv) {
|
|
@@ -40,6 +41,8 @@ function parseArgs(argv) {
|
|
|
40
41
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
42
|
const a = argv[i];
|
|
42
43
|
if (a === '--vault') args.vault = argv[++i];
|
|
44
|
+
else if (a === '--locale') args.locale = argv[++i];
|
|
45
|
+
else if (a.startsWith('--locale=')) args.locale = a.slice(9);
|
|
43
46
|
else if (a === '--project') args.project = argv[++i];
|
|
44
47
|
else if (a === '--no-mcp') args.mcp = false;
|
|
45
48
|
else if (a === '--yes' || a === '-y') args.yes = true;
|
|
@@ -162,10 +165,11 @@ function runCavemanInstaller(log) {
|
|
|
162
165
|
// enable it (non-destructive), and add graph color groups. Returns a short note.
|
|
163
166
|
// Unparseable user JSON is left untouched (we only merge into valid/absent files).
|
|
164
167
|
function installVaultColors(vaultPath) {
|
|
168
|
+
const loc = getLocale(vaultPath);
|
|
165
169
|
const obsidianDir = join(vaultPath, '.obsidian');
|
|
166
170
|
const snippetsDir = join(obsidianDir, 'snippets');
|
|
167
171
|
mkdirSync(snippetsDir, { recursive: true });
|
|
168
|
-
writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(), 'utf8');
|
|
172
|
+
writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(loc), 'utf8');
|
|
169
173
|
|
|
170
174
|
let enabled = false;
|
|
171
175
|
const appPath = join(obsidianDir, 'appearance.json');
|
|
@@ -179,9 +183,9 @@ function installVaultColors(vaultPath) {
|
|
|
179
183
|
const graphPath = join(obsidianDir, 'graph.json');
|
|
180
184
|
const graphRead = readJsonSafe(graphPath);
|
|
181
185
|
if (graphRead.ok) {
|
|
182
|
-
const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups());
|
|
186
|
+
const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups(loc));
|
|
183
187
|
writeJson(graphPath, merged);
|
|
184
|
-
groups = graphColorGroups().length;
|
|
188
|
+
groups = graphColorGroups(loc).length;
|
|
185
189
|
}
|
|
186
190
|
return `snippet ${SNIPPET_NAME}.css${enabled ? ' (enabled)' : ' (enable by hand: appearance.json unreadable)'} + ${groups} graph group(s)`;
|
|
187
191
|
}
|
|
@@ -253,9 +257,21 @@ export async function runInit(argv) {
|
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
260
|
+
// Locale (0.8.0): a vault property, locked at init. Written BEFORE folder creation so
|
|
261
|
+
// the folder names follow it. Invalid/absent = pt-BR (backward compat).
|
|
256
262
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
263
|
+
const localeId = args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE;
|
|
264
|
+
if (args.locale && !LOCALES[args.locale]) {
|
|
265
|
+
log(` ! locale desconhecido "${args.locale}" — usando ${DEFAULT_LOCALE} (opções: ${Object.keys(LOCALES).join(', ')})`);
|
|
266
|
+
}
|
|
267
|
+
mkdirSync(join(vaultPath, '.brain'), { recursive: true });
|
|
268
|
+
const configPath = join(vaultPath, '.brain', 'config.json');
|
|
269
|
+
if (!existsSync(configPath)) writeFileSync(configPath, `${JSON.stringify({ locale: localeId }, null, 2)}\n`, 'utf8');
|
|
270
|
+
clearLocaleCache();
|
|
271
|
+
const loc = getLocale(vaultPath);
|
|
272
|
+
const folders = vaultFolders(loc);
|
|
257
273
|
let created = 0;
|
|
258
|
-
for (const f of
|
|
274
|
+
for (const f of folders) {
|
|
259
275
|
const p = join(vaultPath, f);
|
|
260
276
|
if (!existsSync(p)) {
|
|
261
277
|
mkdirSync(p, { recursive: true });
|
|
@@ -275,7 +291,7 @@ export async function runInit(argv) {
|
|
|
275
291
|
const brainDir = join(vaultPath, '.brain');
|
|
276
292
|
mkdirSync(brainDir, { recursive: true });
|
|
277
293
|
const corePath = join(brainDir, 'CORE.md');
|
|
278
|
-
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
|
|
294
|
+
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(loc.id), 'utf8');
|
|
279
295
|
const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
|
|
280
296
|
if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
|
|
281
297
|
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
@@ -283,8 +299,8 @@ export async function runInit(argv) {
|
|
|
283
299
|
seedDefinitions(brainDir);
|
|
284
300
|
seedWkSkills(brainDir); // Pilar A: native process skills (wk-workflow/tdd/debugging/...).
|
|
285
301
|
// Seed the change/spec layer starters (Pilar B) — non-destructive.
|
|
286
|
-
const specsReadme = join(vaultPath,
|
|
287
|
-
if (!existsSync(specsReadme)) writeFileSync(specsReadme,
|
|
302
|
+
const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
|
|
303
|
+
if (!existsSync(specsReadme)) writeFileSync(specsReadme, `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
|
|
288
304
|
const changeTpl = join(vaultPath, 'Templates', 'Change.md');
|
|
289
305
|
if (!existsSync(changeTpl)) writeFileSync(changeTpl, '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
|
|
290
306
|
// Seed the native sensor config (Pilar C) at project root — non-destructive.
|
|
@@ -294,7 +310,7 @@ export async function runInit(argv) {
|
|
|
294
310
|
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
295
311
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
296
312
|
}
|
|
297
|
-
log(` [1/4] vault taxonomy: ${
|
|
313
|
+
log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded`);
|
|
298
314
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
299
315
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
300
316
|
const synced = syncDefs(vaultPath, projectPath);
|
package/src/sensors.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// `wendkeep sensors list` — read-only view over wendkeep.sensors.json (0.7.0).
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { loadSensors } from '../hooks/sensors-core.mjs';
|
|
4
|
+
|
|
5
|
+
export function runSensors(argv) {
|
|
6
|
+
const [sub, ...rest] = argv;
|
|
7
|
+
if (sub !== 'list') {
|
|
8
|
+
process.stderr.write(`wendkeep sensors: unknown subcommand "${sub}". Known: list.\n`);
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
let project;
|
|
12
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
13
|
+
if (rest[i] === '--project') project = rest[++i];
|
|
14
|
+
else if (rest[i].startsWith('--project=')) project = rest[i].slice(10);
|
|
15
|
+
}
|
|
16
|
+
const projectRoot = resolve(project || process.cwd());
|
|
17
|
+
const sensors = loadSensors(projectRoot);
|
|
18
|
+
if (!sensors.length) { process.stdout.write('sensors: (nenhum — crie wendkeep.sensors.json na raiz)\n'); process.exit(0); }
|
|
19
|
+
for (const s of sensors) {
|
|
20
|
+
process.stdout.write(`${s.id}: ${s.type || 'command'} · ${s.severity || 'critical'} · ${s.command}\n`);
|
|
21
|
+
}
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
package/src/spec.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { parseRequirements } from '../hooks/spec-core.mjs';
|
|
5
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
6
|
+
|
|
7
|
+
function resolveVault(argv) {
|
|
8
|
+
let vault;
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const a = argv[i];
|
|
11
|
+
if (a === '--vault') vault = argv[++i];
|
|
12
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
13
|
+
}
|
|
14
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
15
|
+
if (!base) {
|
|
16
|
+
process.stderr.write('wendkeep spec: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
return isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runSpec(argv) {
|
|
23
|
+
const [sub, ...rest] = argv;
|
|
24
|
+
const vaultBase = resolveVault(rest);
|
|
25
|
+
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
26
|
+
|
|
27
|
+
if (sub === 'list') {
|
|
28
|
+
let files = [];
|
|
29
|
+
try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
|
|
30
|
+
if (!files.length) { process.stdout.write('specs: (nenhuma)\n'); process.exit(0); }
|
|
31
|
+
for (const f of files) {
|
|
32
|
+
const md = readFileSync(join(specsDir, f), 'utf8');
|
|
33
|
+
const n = parseRequirements(md).length;
|
|
34
|
+
const upd = md.match(/> Atualizado por .* em (\d{4}-\d{2}-\d{2})/);
|
|
35
|
+
process.stdout.write(`${f.replace(/\.md$/, '')}: ${n} requisito(s)${upd ? ` (atualizado ${upd[1]})` : ''}\n`);
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sub === 'show') {
|
|
41
|
+
const cap = rest.find((a, i) => !a.startsWith('-') && rest[i - 1] !== '--vault');
|
|
42
|
+
if (!cap) { process.stderr.write('wendkeep spec show: missing <capability>\n'); process.exit(2); }
|
|
43
|
+
let md;
|
|
44
|
+
try { md = readFileSync(join(specsDir, `${cap}.md`), 'utf8'); }
|
|
45
|
+
catch { process.stderr.write(`wendkeep spec show: not found: ${cap}\n`); process.exit(2); }
|
|
46
|
+
const reqs = parseRequirements(md);
|
|
47
|
+
process.stdout.write(`${cap}: ${reqs.length} requisito(s)\n`);
|
|
48
|
+
for (const r of reqs) process.stdout.write(` ${r.id ? `${r.id} — ` : ''}${r.name}\n`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
package/src/sync-defs.mjs
CHANGED
|
@@ -5,11 +5,67 @@
|
|
|
5
5
|
// .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
|
|
6
6
|
// .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
|
|
7
7
|
// cross-platform robustness.
|
|
8
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
|
|
11
|
+
// Managed AGENTS.md section (0.8.0): the agent-agnostic distribution channel. Codex, Amp,
|
|
12
|
+
// Cursor, Zed et al. read AGENTS.md — one file covers them all. Only the content between
|
|
13
|
+
// the markers is ours; user content around it is never touched.
|
|
14
|
+
const AG_START = '<!-- wendkeep:skills:start -->';
|
|
15
|
+
const AG_END = '<!-- wendkeep:skills:end -->';
|
|
16
|
+
|
|
17
|
+
function skillInventory(skillsSrc) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let names = [];
|
|
20
|
+
try { names = readdirSync(skillsSrc); } catch { return out; }
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
try {
|
|
23
|
+
if (!statSync(join(skillsSrc, name)).isDirectory()) continue;
|
|
24
|
+
const md = readFileSync(join(skillsSrc, name, 'SKILL.md'), 'utf8');
|
|
25
|
+
const desc = (md.match(/^description:\s*(.+)$/m) || [])[1] || '';
|
|
26
|
+
out.push({ name, description: desc.trim() });
|
|
27
|
+
} catch { /* skill sem SKILL.md */ }
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderAgentsSection(skills) {
|
|
33
|
+
const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
|
|
34
|
+
return `${AG_START}
|
|
35
|
+
## wendkeep — process skills & loop
|
|
36
|
+
|
|
37
|
+
This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
|
|
38
|
+
through its change loop: \`wendkeep change new <slug>\` → implement tasks test-first
|
|
39
|
+
(tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
|
|
40
|
+
\`wendkeep verify --deep\` + an independent read-only verification pass writing
|
|
41
|
+
\`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
|
|
42
|
+
status\` / \`spec list\` / \`sensors list\`.
|
|
43
|
+
|
|
44
|
+
Process skills (full text in \`.claude/skills/\` and the vault's \`.brain/skills/\`):
|
|
45
|
+
${list}
|
|
46
|
+
${AG_END}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function upsertAgentsMd(projectPath, skillsSrc) {
|
|
50
|
+
const skills = skillInventory(skillsSrc);
|
|
51
|
+
if (!skills.length) return false;
|
|
52
|
+
const path = join(projectPath, 'AGENTS.md');
|
|
53
|
+
const section = renderAgentsSection(skills);
|
|
54
|
+
let content = '';
|
|
55
|
+
try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
|
|
56
|
+
if (content.includes(AG_START) && content.includes(AG_END)) {
|
|
57
|
+
const start = content.indexOf(AG_START);
|
|
58
|
+
const end = content.indexOf(AG_END) + AG_END.length;
|
|
59
|
+
content = content.slice(0, start) + section + content.slice(end);
|
|
60
|
+
} else {
|
|
61
|
+
content = content ? `${content.trimEnd()}\n\n${section}\n` : `${section}\n`;
|
|
62
|
+
}
|
|
63
|
+
writeFileSync(path, content, 'utf8');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
11
67
|
export function syncDefs(vaultBase, projectPath) {
|
|
12
|
-
const out = { agents: [], skills: [] };
|
|
68
|
+
const out = { agents: [], skills: [], agentsMd: false };
|
|
13
69
|
|
|
14
70
|
const agentsSrc = join(vaultBase, '.brain', 'agents');
|
|
15
71
|
if (existsSync(agentsSrc)) {
|
|
@@ -34,6 +90,9 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
34
90
|
}
|
|
35
91
|
}
|
|
36
92
|
|
|
93
|
+
// Agent-agnostic channel: the managed AGENTS.md section (docs/17).
|
|
94
|
+
out.agentsMd = upsertAgentsMd(projectPath, skillsSrc);
|
|
95
|
+
|
|
37
96
|
return out;
|
|
38
97
|
}
|
|
39
98
|
|
package/src/taxonomy.mjs
CHANGED
package/src/validate-core.mjs
CHANGED
|
@@ -11,11 +11,20 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
11
11
|
const HARD_LIMIT = 25;
|
|
12
12
|
const SOFT_LIMIT = 22;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
]
|
|
14
|
+
// Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
|
|
15
|
+
// locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
|
|
16
|
+
const SECTION_SETS = {
|
|
17
|
+
'pt-BR': [
|
|
18
|
+
{ label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
|
|
19
|
+
{ label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
|
|
20
|
+
{ label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
|
|
21
|
+
],
|
|
22
|
+
en: [
|
|
23
|
+
{ label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
|
|
24
|
+
{ label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
|
|
25
|
+
{ label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
19
28
|
|
|
20
29
|
// Secret patterns reject only "real" values (length floor); abstract mentions like
|
|
21
30
|
// `sk_*` / `whsec_*` (trailing asterisk) are allowed.
|
|
@@ -41,9 +50,10 @@ export function validateCore(content) {
|
|
|
41
50
|
if (lineCount > HARD_LIMIT) {
|
|
42
51
|
errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
43
52
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Pick the locale set that matches best; require it to be complete.
|
|
54
|
+
const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
|
|
55
|
+
const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
|
|
56
|
+
for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
|
|
47
57
|
for (const { name, regex } of SECRET_PATTERNS) {
|
|
48
58
|
const m = text.match(regex);
|
|
49
59
|
if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
|
|
@@ -61,7 +71,22 @@ export function validateCore(content) {
|
|
|
61
71
|
|
|
62
72
|
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
63
73
|
// curated hot layer exists with the right shape from day one.
|
|
64
|
-
export function renderCoreSkeleton() {
|
|
74
|
+
export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
75
|
+
if (localeId === 'en') {
|
|
76
|
+
return `# CORE — curated memory core (.brain)
|
|
77
|
+
|
|
78
|
+
> RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
|
|
79
|
+
|
|
80
|
+
## User Preferences
|
|
81
|
+
- (durable preferences: language, style, conventions)
|
|
82
|
+
|
|
83
|
+
## Active Patterns
|
|
84
|
+
- (active patterns/architecture another agent must know)
|
|
85
|
+
|
|
86
|
+
## Open Items
|
|
87
|
+
- (open items/decisions — remove when resolved)
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
65
90
|
return `# CORE — núcleo curado da memória (.brain)
|
|
66
91
|
|
|
67
92
|
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
package/src/vault-theme.mjs
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
export const SNIPPET_NAME = 'wendkeep-colors';
|
|
10
10
|
|
|
11
|
+
import { LOCALES } from '../hooks/locale.mjs';
|
|
12
|
+
const PT = LOCALES['pt-BR'];
|
|
13
|
+
|
|
11
14
|
// Base palette (hex + "r, g, b" for rgba()).
|
|
12
15
|
const PALETTE = {
|
|
13
16
|
blue: { hex: '#2f80ed', rgb: '47, 128, 237' },
|
|
@@ -20,42 +23,49 @@ const PALETTE = {
|
|
|
20
23
|
slate: { hex: '#64748b', rgb: '100, 116, 139' },
|
|
21
24
|
};
|
|
22
25
|
|
|
23
|
-
// File-explorer folder -> palette key
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
// File-explorer folder KEY -> palette key; folder names resolve via the vault locale.
|
|
27
|
+
const PALETTE_BY_KEY = {
|
|
28
|
+
inbox: 'amber',
|
|
29
|
+
project: 'indigo',
|
|
30
|
+
sessions: 'blue',
|
|
31
|
+
linear: 'teal',
|
|
32
|
+
decisions: 'violet',
|
|
33
|
+
bugs: 'red',
|
|
34
|
+
learnings: 'green',
|
|
35
|
+
specs: 'teal',
|
|
36
|
+
changes: 'amber',
|
|
37
|
+
};
|
|
38
|
+
function folderPalette(loc) {
|
|
39
|
+
return [...Object.entries(PALETTE_BY_KEY).map(([k, p]) => [loc.folders[k], p]), ['Templates', 'slate']];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Note cssclass -> palette + locale folder key; `folder` resolves per locale.
|
|
43
|
+
const NOTE_TYPES = {
|
|
44
|
+
session: { cssClass: 'topic-session', folderKey: 'sessions', palette: 'blue' },
|
|
45
|
+
decision: { cssClass: 'topic-decision', folderKey: 'decisions', palette: 'violet' },
|
|
46
|
+
bug: { cssClass: 'topic-bug', folderKey: 'bugs', palette: 'red' },
|
|
47
|
+
learning: { cssClass: 'topic-learning', folderKey: 'learnings', palette: 'green' },
|
|
48
|
+
spec: { cssClass: 'topic-spec', folderKey: 'specs', palette: 'teal' },
|
|
49
|
+
change: { cssClass: 'topic-change', folderKey: 'changes', palette: 'amber' },
|
|
45
50
|
};
|
|
51
|
+
// Legacy pt-shaped export (folder names resolved at pt-BR) kept for compat.
|
|
52
|
+
export const NOTE_COLORS = Object.fromEntries(
|
|
53
|
+
Object.entries(NOTE_TYPES).map(([k, v]) => [k, { cssClass: v.cssClass, folder: PT.folders[v.folderKey], palette: v.palette }]),
|
|
54
|
+
);
|
|
46
55
|
|
|
47
|
-
// cssclasses that get the note-accent treatment (the
|
|
48
|
-
const TOPIC_CLASSES = [...Object.values(
|
|
56
|
+
// cssclasses that get the note-accent treatment (the note types + a home note).
|
|
57
|
+
const TOPIC_CLASSES = [...Object.values(NOTE_TYPES).map((n) => n.cssClass), 'vault-home'];
|
|
49
58
|
const NOTE_ACCENTS = [
|
|
50
|
-
...Object.values(
|
|
59
|
+
...Object.values(NOTE_TYPES).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
|
|
51
60
|
{ cssClass: 'vault-home', palette: 'indigo' },
|
|
52
61
|
];
|
|
53
62
|
|
|
54
63
|
const FX = '.workspace-leaf-content[data-type="file-explorer"] .nav-folder-title';
|
|
55
|
-
const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
|
|
56
64
|
const topicReading = TOPIC_CLASSES.map((c) => `.${c}`).join(', ');
|
|
57
65
|
|
|
58
|
-
export function renderColorSnippetCss() {
|
|
66
|
+
export function renderColorSnippetCss(loc = PT) {
|
|
67
|
+
const FOLDER_PALETTE = folderPalette(loc);
|
|
68
|
+
const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
|
|
59
69
|
const rootVars = Object.entries(PALETTE)
|
|
60
70
|
.map(([k, v]) => ` --us-${k}: ${v.hex};\n --us-${k}-rgb: ${v.rgb};`)
|
|
61
71
|
.join('\n');
|
|
@@ -181,9 +191,9 @@ export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
// Graph color groups: one per note folder, colored from the palette.
|
|
184
|
-
export function graphColorGroups() {
|
|
185
|
-
return Object.values(
|
|
186
|
-
query: `path:"${v.
|
|
194
|
+
export function graphColorGroups(loc = PT) {
|
|
195
|
+
return Object.values(NOTE_TYPES).map((v) => ({
|
|
196
|
+
query: `path:"${loc.folders[v.folderKey]}"`,
|
|
187
197
|
color: { a: 1, rgb: parseInt(PALETTE[v.palette].hex.slice(1), 16) },
|
|
188
198
|
}));
|
|
189
199
|
}
|
package/src/verify.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.m
|
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
import { tasksHashOf } from '../hooks/spec-core.mjs';
|
|
9
9
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
10
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
10
11
|
|
|
11
12
|
function today() {
|
|
12
13
|
const d = new Date();
|
|
@@ -28,7 +29,7 @@ export function runVerify(argv) {
|
|
|
28
29
|
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
29
30
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
30
31
|
|
|
31
|
-
const changeDir = join(vaultBase,
|
|
32
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
32
33
|
let tarefas = '';
|
|
33
34
|
try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
|
|
34
35
|
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|