wendkeep 0.7.0 → 0.8.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 +38 -0
- package/README.md +1 -1
- package/hooks/brain-core.mjs +2 -1
- package/hooks/change-core.mjs +34 -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 -2
- package/src/change.mjs +6 -5
- package/src/init.mjs +36 -11
- package/src/skills-seed.mjs +146 -3
- package/src/spec.mjs +2 -1
- package/src/sync-defs.mjs +61 -2
- package/src/taxonomy.mjs +1 -0
- package/src/validate-core.mjs +34 -9
- package/src/vault-readme.mjs +86 -44
- package/src/vault-theme.mjs +40 -30
- package/src/verify.mjs +2 -1
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 });
|
|
@@ -266,7 +282,7 @@ export async function runInit(argv) {
|
|
|
266
282
|
const readmePath = join(vaultPath, 'README.md');
|
|
267
283
|
let readmeNote = '';
|
|
268
284
|
if (!existsSync(readmePath)) {
|
|
269
|
-
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
|
|
285
|
+
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp, locale: loc.id }), 'utf8');
|
|
270
286
|
readmeNote = ', README.md created';
|
|
271
287
|
}
|
|
272
288
|
// Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
|
|
@@ -275,18 +291,27 @@ 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
|
|
282
298
|
// truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
|
|
283
299
|
seedDefinitions(brainDir);
|
|
284
|
-
seedWkSkills(brainDir); // Pilar A: native process skills
|
|
300
|
+
seedWkSkills(brainDir, loc.id); // Pilar A: native process skills, in the vault locale.
|
|
285
301
|
// Seed the change/spec layer starters (Pilar B) — non-destructive.
|
|
286
|
-
const
|
|
287
|
-
|
|
302
|
+
const en = loc.id === 'en';
|
|
303
|
+
const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
|
|
304
|
+
if (!existsSync(specsReadme)) {
|
|
305
|
+
writeFileSync(specsReadme, en
|
|
306
|
+
? `# Specs — living contract\n\nThe project's capabilities (requirements/scenarios). Changes in \`${loc.folders.changes}/\` promote deltas here on \`wendkeep change archive\`.\n`
|
|
307
|
+
: `# Specs — contrato vivo\n\nCapacidades do projeto (requisitos/cenários). Changes em \`${loc.folders.changes}/\` promovem deltas aqui no \`wendkeep change archive\`.\n`, 'utf8');
|
|
308
|
+
}
|
|
288
309
|
const changeTpl = join(vaultPath, 'Templates', 'Change.md');
|
|
289
|
-
if (!existsSync(changeTpl))
|
|
310
|
+
if (!existsSync(changeTpl)) {
|
|
311
|
+
writeFileSync(changeTpl, en
|
|
312
|
+
? '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Why\n\n## What changes\n'
|
|
313
|
+
: '---\ntype: change\nstatus: active\ncssclasses:\n - topic-change\n---\n\n# <slug>\n\n## Por quê\n\n## O que muda\n', 'utf8');
|
|
314
|
+
}
|
|
290
315
|
// Seed the native sensor config (Pilar C) at project root — non-destructive.
|
|
291
316
|
const sensorsFile = join(projectPath, 'wendkeep.sensors.json');
|
|
292
317
|
if (!existsSync(sensorsFile)) {
|
|
@@ -294,7 +319,7 @@ export async function runInit(argv) {
|
|
|
294
319
|
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
295
320
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
296
321
|
}
|
|
297
|
-
log(` [1/4] vault taxonomy: ${
|
|
322
|
+
log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded`);
|
|
298
323
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
299
324
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
300
325
|
const synced = syncDefs(vaultPath, projectPath);
|
package/src/skills-seed.mjs
CHANGED
|
@@ -203,7 +203,135 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
|
|
|
203
203
|
- O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
|
|
204
204
|
`;
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
const WORKFLOW_EN = `# The a2 loop — wendkeep's work cycle
|
|
207
|
+
|
|
208
|
+
Use it when starting any non-trivial change. The loop keeps memory (vault) and proof
|
|
209
|
+
(sensors) together, wikilinked in the Obsidian graph.
|
|
210
|
+
|
|
211
|
+
## Steps
|
|
212
|
+
|
|
213
|
+
1. **Explore** — understand the problem before proposing.
|
|
214
|
+
2. **Propose** — \`wendkeep change new <slug>\` scaffolds \`08-Changes/<slug>/\`
|
|
215
|
+
(proposta/design/tarefas + a \`specs/\` delta). The change becomes *active* and is
|
|
216
|
+
injected at the next SessionStart.
|
|
217
|
+
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
218
|
+
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
|
|
219
|
+
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
220
|
+
the verification package.
|
|
221
|
+
5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
|
|
222
|
+
A trivial change (no \`[req:]\`) gets an auto verdict.
|
|
223
|
+
6. **Archive** — \`wendkeep change archive <slug>\`. The gate needs green sensors AND a
|
|
224
|
+
verdict AND no open tasks. It promotes the delta into \`07-Specs\` and mints an ADR.
|
|
225
|
+
|
|
226
|
+
## Rules
|
|
227
|
+
- One active change at a time. Finish (archive) before starting another.
|
|
228
|
+
- No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
|
|
229
|
+
- The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const TDD_EN = `# TDD — Red, Green, Refactor (tests that discriminate)
|
|
233
|
+
|
|
234
|
+
Never write production code without a red test asking for it — and never write a test that
|
|
235
|
+
would pass under the wrong implementation.
|
|
236
|
+
|
|
237
|
+
## The cycle
|
|
238
|
+
1. **Red** — the smallest test that fails for the missing behaviour.
|
|
239
|
+
2. **See it fail** for the right reason (not an import/typo).
|
|
240
|
+
3. **Green** — the minimal code to pass.
|
|
241
|
+
4. **Refactor** with the greens protecting you.
|
|
242
|
+
|
|
243
|
+
## Derive from the spec, not the code
|
|
244
|
+
Write assertions from the requirement's acceptance criteria (\`07-Specs\`), not by reading the
|
|
245
|
+
implementation. Reading the code to write the test = it only confirms what the code already does.
|
|
246
|
+
|
|
247
|
+
## Non-shallow litmus
|
|
248
|
+
Reject an assertion that would pass under a wrong implementation. Assert a **value or persisted
|
|
249
|
+
state**, never "the mock was called". "Tested later" is rejected.
|
|
250
|
+
|
|
251
|
+
## Adequacy (necessary and sufficient)
|
|
252
|
+
Every acceptance criterion has an assertion with \`file:line\` evidence; every test traces to a
|
|
253
|
+
requirement (\`[req:ID]\`); covers what the spec asks, nothing more.
|
|
254
|
+
|
|
255
|
+
## Learn the project
|
|
256
|
+
Sample 5–10 existing tests for style/location/framework/depth (treat that depth as a floor).
|
|
257
|
+
Read \`AGENTS.md\` / \`.cursor/rules\` / CI configs for declared standards.
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const DEBUGGING_EN = `# Systematic debugging
|
|
261
|
+
|
|
262
|
+
Use it when something fails or behaves wrong. One hypothesis at a time — no shotgun changes.
|
|
263
|
+
|
|
264
|
+
1. **Reproduce** — a minimal deterministic case. Without it you can't know you fixed anything.
|
|
265
|
+
2. **One hypothesis** — the most likely cause, specific enough to test.
|
|
266
|
+
3. **Isolate** — confirm or kill the hypothesis BEFORE fixing (bisect, log, comment half). Prove
|
|
267
|
+
where it is, not where you think it is.
|
|
268
|
+
4. **Fix the root**, not the symptom.
|
|
269
|
+
5. **Verify** — the repro is gone AND the suite stays green. No regression.
|
|
270
|
+
|
|
271
|
+
Changed several things and it "worked"? You don't know what fixed it — revert, isolate one
|
|
272
|
+
variable. Read the whole error + stack — the decisive line is usually there.
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
const BRAINSTORMING_EN = `# Brainstorming — idea into design
|
|
276
|
+
|
|
277
|
+
Use it when the idea is still vague. Turn it into an approved design BEFORE writing code.
|
|
278
|
+
|
|
279
|
+
1. **Explore context** — files, docs, relevant history.
|
|
280
|
+
2. **One question at a time** — purpose, constraints, success criteria. Prefer multiple choice.
|
|
281
|
+
3. **Propose 2–3 approaches** with trade-offs and your recommendation.
|
|
282
|
+
4. **Present the design in sections**, confirm each.
|
|
283
|
+
|
|
284
|
+
## Closure gate — no dangling ambiguity
|
|
285
|
+
Resolve every gray area: decide with the user, or log a **signed-off assumption** ("assuming X
|
|
286
|
+
because Y — correct me if wrong"). A declined gray area is recorded, not silently dropped.
|
|
287
|
+
|
|
288
|
+
## Out-of-scope table
|
|
289
|
+
List explicitly what the change does **not** do. Undeclared scope becomes creep.
|
|
290
|
+
|
|
291
|
+
## Hard gate
|
|
292
|
+
No code / scaffold / implementation action until a design is presented and approved. Then go to
|
|
293
|
+
**wk-planning**.
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
const PLANNING_EN = `# Planning — design into a task plan
|
|
297
|
+
|
|
298
|
+
Use it after an approved design. Produce a plan an engineer with no project context can execute.
|
|
299
|
+
|
|
300
|
+
## Before tasks
|
|
301
|
+
Map the files: what to create/modify and each one's responsibility. Files that change together
|
|
302
|
+
live together; one file, one responsibility.
|
|
303
|
+
|
|
304
|
+
## Bite-sized tasks (TDD)
|
|
305
|
+
Each task ends in an independently testable deliverable. Each step is a 2–5 min action:
|
|
306
|
+
write the failing test (show code) → run and see it fail (exact command + expected) → minimal
|
|
307
|
+
implementation (show code) → run and see it pass → checkpoint: suite green.
|
|
308
|
+
|
|
309
|
+
## Rules
|
|
310
|
+
- Exact file paths, always. Real code in each step — no "TODO", "handle errors appropriately",
|
|
311
|
+
"similar to Task N". DRY, YAGNI. Consistent names/signatures across tasks.
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
const VERIFY_EN = `# Independent verification — the fresh pass
|
|
315
|
+
|
|
316
|
+
Use it in the *verify deep* step, after \`wendkeep verify --deep\`. You are the **verifier**, not
|
|
317
|
+
the author — even if you wrote the code, enter as if you'd never seen it. Fresh context, read-only.
|
|
318
|
+
|
|
319
|
+
## What to do
|
|
320
|
+
1. Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence).
|
|
321
|
+
2. **Re-derive coverage from \`07-Specs\`** — for each \`[req:ID]\`, check the requirement's behaviour
|
|
322
|
+
is covered by a test that discriminates (wouldn't pass under a wrong impl). \`file:line\` evidence.
|
|
323
|
+
3. Spec-anchored outcome check: does the observable result match the acceptance criterion?
|
|
324
|
+
4. Write \`08-Changes/<slug>/verdict.json\`:
|
|
325
|
+
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "notes": [] }\`.
|
|
326
|
+
|
|
327
|
+
## Rules
|
|
328
|
+
- **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
|
|
329
|
+
- \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
|
|
330
|
+
- Don't fix here — a gap becomes a fix task; re-verify after.
|
|
331
|
+
- The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
const WK_SKILLS_PT = [
|
|
207
335
|
skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
|
|
208
336
|
skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
|
|
209
337
|
skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
|
|
@@ -212,10 +340,25 @@ export const WK_SKILLS = [
|
|
|
212
340
|
skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
|
|
213
341
|
];
|
|
214
342
|
|
|
343
|
+
const WK_SKILLS_EN = [
|
|
344
|
+
skill('wk-workflow', 'Use when starting any non-trivial change — orchestrates the a2 loop (explore, propose, apply, verify, archive) over the wendkeep commands.', WORKFLOW_EN),
|
|
345
|
+
skill('wk-tdd', 'Use when implementing any behaviour — Red/Green/Refactor with tests that discriminate (spec-derived, non-shallow litmus, adequacy).', TDD_EN),
|
|
346
|
+
skill('wk-debugging', 'Use when something fails or breaks — systematic hypothesis-driven debugging before fixing.', DEBUGGING_EN),
|
|
347
|
+
skill('wk-brainstorming', 'Use when the idea is still vague — turns it into an approved design, with a closure gate and out-of-scope table, before code.', BRAINSTORMING_EN),
|
|
348
|
+
skill('wk-planning', 'Use after an approved design — decomposes it into a bite-sized TDD task plan.', PLANNING_EN),
|
|
349
|
+
skill('wk-verify', 'Use in verify deep — an independent read-only pass (author≠verifier) that re-derives spec coverage and writes verdict.json.', VERIFY_EN),
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
// Skill set for a locale. WK_SKILLS stays the pt-BR set for back-compat.
|
|
353
|
+
export function wkSkills(localeId = 'pt-BR') {
|
|
354
|
+
return localeId === 'en' ? WK_SKILLS_EN : WK_SKILLS_PT;
|
|
355
|
+
}
|
|
356
|
+
export const WK_SKILLS = WK_SKILLS_PT;
|
|
357
|
+
|
|
215
358
|
// Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
|
|
216
|
-
export function seedWkSkills(brainDir) {
|
|
359
|
+
export function seedWkSkills(brainDir, localeId = 'pt-BR') {
|
|
217
360
|
const created = [];
|
|
218
|
-
for (const s of
|
|
361
|
+
for (const s of wkSkills(localeId)) {
|
|
219
362
|
const dir = join(brainDir, 'skills', s.name);
|
|
220
363
|
mkdirSync(dir, { recursive: true });
|
|
221
364
|
const f = join(dir, 'SKILL.md');
|
package/src/spec.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { parseRequirements } from '../hooks/spec-core.mjs';
|
|
5
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
5
6
|
|
|
6
7
|
function resolveVault(argv) {
|
|
7
8
|
let vault;
|
|
@@ -21,7 +22,7 @@ function resolveVault(argv) {
|
|
|
21
22
|
export function runSpec(argv) {
|
|
22
23
|
const [sub, ...rest] = argv;
|
|
23
24
|
const vaultBase = resolveVault(rest);
|
|
24
|
-
const specsDir = join(vaultBase,
|
|
25
|
+
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
25
26
|
|
|
26
27
|
if (sub === 'list') {
|
|
27
28
|
let files = [];
|
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-readme.mjs
CHANGED
|
@@ -1,46 +1,90 @@
|
|
|
1
1
|
// Renders a neutral, project-templated README for a freshly created wendkeep vault.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
// Locale-aware (0.8.1): the structure table + prose follow the vault locale; folder names
|
|
3
|
+
// come from the locale so they can never drift from what `wendkeep init` creates.
|
|
4
|
+
import { LOCALES, vaultFolders } from '../hooks/locale.mjs';
|
|
5
|
+
|
|
6
|
+
// Folder descriptions keyed by locale folder KEY (not literal name), per locale.
|
|
7
|
+
const DESC = {
|
|
8
|
+
'pt-BR': {
|
|
9
|
+
inbox: 'Notas rápidas, captura sem classificação',
|
|
10
|
+
project: 'Arquitetura, padrões, memória do projeto',
|
|
11
|
+
sessions: 'Uma nota por sessão de trabalho com o agente IA',
|
|
12
|
+
linear: 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
|
|
13
|
+
decisions: 'Architecture Decision Records (ADRs)',
|
|
14
|
+
bugs: 'Bugs resolvidos com causa raiz documentada',
|
|
15
|
+
learnings: 'Lições extraídas das sessões',
|
|
16
|
+
specs: 'Contrato vivo: capacidades do projeto (requisitos)',
|
|
17
|
+
changes: 'Mudanças em andamento (proposta/design/tarefas/spec-delta)',
|
|
18
|
+
Templates: 'Templates para novas notas',
|
|
19
|
+
'.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
|
|
20
|
+
},
|
|
21
|
+
en: {
|
|
22
|
+
inbox: 'Quick notes, unclassified capture',
|
|
23
|
+
project: 'Architecture, patterns, project memory',
|
|
24
|
+
sessions: 'One note per AI-agent work session',
|
|
25
|
+
linear: 'Mirror of issues/tasks from your tracker (Linear, Jira, GitHub Issues…)',
|
|
26
|
+
decisions: 'Architecture Decision Records (ADRs)',
|
|
27
|
+
bugs: 'Fixed bugs with documented root cause',
|
|
28
|
+
learnings: 'Lessons extracted from sessions',
|
|
29
|
+
specs: 'Living contract: the project capabilities (requirements)',
|
|
30
|
+
changes: 'Changes in flight (proposal/design/tasks/spec-delta)',
|
|
31
|
+
Templates: 'Templates for new notes',
|
|
32
|
+
'.brain': 'Canonical memory: CORE.md (curated) + DIGEST.md (auto), injected at SessionStart',
|
|
33
|
+
},
|
|
16
34
|
};
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
23
|
-
return ['| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
|
|
36
|
+
// folder literal -> key, for description lookup.
|
|
37
|
+
function keyForFolder(loc, folder) {
|
|
38
|
+
const entry = Object.entries(loc.folders).find(([, name]) => name === folder);
|
|
39
|
+
return entry ? entry[0] : folder; // Templates / .brain fall through as themselves
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
42
|
+
export function renderVaultReadme({ projectName, vaultPath, withMcp = true, locale = 'pt-BR' }) {
|
|
43
|
+
const loc = LOCALES[locale] || LOCALES['pt-BR'];
|
|
44
|
+
const en = loc.id === 'en';
|
|
45
|
+
const desc = DESC[loc.id] || DESC['pt-BR'];
|
|
46
|
+
const name = projectName || (en ? 'project' : 'projeto');
|
|
47
|
+
|
|
48
|
+
const rows = vaultFolders(loc).map((f) => `| \`${f}/\` | ${desc[keyForFolder(loc, f)] || (en ? 'Project notes' : 'Notas do projeto')} |`);
|
|
49
|
+
const table = [en ? '| Folder | Contents |' : '| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
|
|
50
|
+
|
|
51
|
+
if (en) {
|
|
52
|
+
const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
|
|
53
|
+
const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
|
|
54
|
+
if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
|
|
55
|
+
access.push('- **Hooks:** `settings.json` calls `npx wendkeep hook <name>`; the vault is located via `OBSIDIAN_VAULT_PATH` (also written by `wendkeep init`).');
|
|
56
|
+
return `# Obsidian vault — ${name}
|
|
57
|
+
|
|
58
|
+
> Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
|
|
59
|
+
> sessions (**Claude Code**, **Codex**) via session hooks${mcpIntro}.
|
|
60
|
+
|
|
61
|
+
## Structure
|
|
62
|
+
|
|
63
|
+
${table}
|
|
64
|
+
|
|
65
|
+
## Access
|
|
29
66
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
${access.join('\n')}
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
1. Every relevant work session produces a note in \`${loc.folders.sessions}/\` (automatic, via hooks).
|
|
72
|
+
2. Architecture decisions become ADRs in \`${loc.folders.decisions}/\`.
|
|
73
|
+
3. Bugs with a root cause go to \`${loc.folders.bugs}/\`; lessons to \`${loc.folders.learnings}/\`.
|
|
74
|
+
4. Canonical memory: \`.brain/CORE.md\` (curated) + \`.brain/DIGEST.md\` (auto) — injected at SessionStart.
|
|
75
|
+
5. Tags in YAML frontmatter, not inline. Wikilinks \`[[note]]\` to connect notes.
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- **Locale:** this vault is \`en\` (folder names in English). Set at \`wendkeep init --locale\`; a vault is never renamed after.
|
|
80
|
+
- **Versioning:** session notes may contain transcript excerpts — review before committing this vault to a shared repo.
|
|
81
|
+
`;
|
|
38
82
|
}
|
|
39
|
-
access.push(
|
|
40
|
-
'- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado\n' +
|
|
41
|
-
' via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).',
|
|
42
|
-
);
|
|
43
83
|
|
|
84
|
+
const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
|
|
85
|
+
const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
|
|
86
|
+
if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
|
|
87
|
+
access.push('- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).');
|
|
44
88
|
return `# Vault Obsidian — ${name}
|
|
45
89
|
|
|
46
90
|
> Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
|
|
@@ -49,7 +93,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
|
|
|
49
93
|
|
|
50
94
|
## Estrutura
|
|
51
95
|
|
|
52
|
-
${
|
|
96
|
+
${table}
|
|
53
97
|
|
|
54
98
|
## Acesso
|
|
55
99
|
|
|
@@ -57,17 +101,15 @@ ${access.join('\n')}
|
|
|
57
101
|
|
|
58
102
|
## Regras
|
|
59
103
|
|
|
60
|
-
1. Toda sessão de trabalho relevante gera nota em
|
|
61
|
-
2. Decisões de arquitetura viram ADR em
|
|
62
|
-
3. Bugs com causa raiz vão para
|
|
104
|
+
1. Toda sessão de trabalho relevante gera nota em \`${loc.folders.sessions}/\` (automático, pelos hooks).
|
|
105
|
+
2. Decisões de arquitetura viram ADR em \`${loc.folders.decisions}/\`.
|
|
106
|
+
3. Bugs com causa raiz vão para \`${loc.folders.bugs}/\`; lições para \`${loc.folders.learnings}/\`.
|
|
63
107
|
4. Memória canônica: \`.brain/CORE.md\` (curado) + \`.brain/DIGEST.md\` (auto) — injetados no SessionStart.
|
|
64
|
-
5. Tags em frontmatter YAML, não inline.
|
|
65
|
-
6. Wikilinks \`[[nota]]\` para conectar notas entre si.
|
|
108
|
+
5. Tags em frontmatter YAML, não inline. Wikilinks \`[[nota]]\` para conectar notas.
|
|
66
109
|
|
|
67
110
|
## Notas
|
|
68
111
|
|
|
69
|
-
- **
|
|
70
|
-
- **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie
|
|
71
|
-
antes de commitar este vault em um repositório compartilhado.
|
|
112
|
+
- **Locale:** este vault é \`pt-BR\` (nomes de pasta em português). Definido em \`wendkeep init --locale\`; o vault nunca é renomeado depois.
|
|
113
|
+
- **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie antes de commitar este vault em um repositório compartilhado.
|
|
72
114
|
`;
|
|
73
115
|
}
|