wendkeep 0.39.0 → 0.41.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 +58 -0
- package/bin/wendkeep.mjs +28 -0
- package/hooks/change-core.mjs +7 -7
- package/hooks/harness-doctor.mjs +1 -1
- package/hooks/linked-notes.mjs +97 -10
- package/hooks/obsidian-common.mjs +12 -5
- package/hooks/renumber-decisions.mjs +1 -1
- package/hooks/renumber-derived.mjs +187 -0
- package/hooks/sensors-core.mjs +28 -6
- package/hooks/session-stop.mjs +12 -6
- package/hooks/spec-core.mjs +17 -3
- package/package.json +2 -2
- package/src/change.mjs +5 -5
- package/src/import.mjs +15 -0
- package/src/note.mjs +80 -0
- package/src/renumber.mjs +23 -9
- package/src/skills-seed.mjs +29 -3
- package/src/spec.mjs +1 -1
- package/src/taxonomy.mjs +1 -0
- package/src/verify.mjs +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,64 @@ 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.41.0] — 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Notas derivadas numeradas: bug e aprendizado gerados automaticamente nascem como
|
|
12
|
+
`BUG-NNNN-<slug>.md` / `APR-NNNN-<slug>.md` na pasta do mês (nunca subpasta `DIA N`),
|
|
13
|
+
com frontmatter `bug:`/`apr:` e H1 `# BUG-0001 — <título>` — paridade com o ADR de
|
|
14
|
+
04-Decisões. Numeração via `getNextDerivedNumber` (scan recursivo, max+1; `getNextAdrNumber`
|
|
15
|
+
virou wrapper).
|
|
16
|
+
- `wendkeep note new --type bug|learning "<título>"`: cria a nota manual já numerada no
|
|
17
|
+
path certo (respeitando locale), com backlink da sessão ativa, e imprime o path — o
|
|
18
|
+
agente nunca calcula número nem pasta à mão. `--date YYYY-MM-DD` opcional.
|
|
19
|
+
- `wendkeep renumber-bugs` e `wendkeep renumber-learnings`: migração retroativa — preview
|
|
20
|
+
por default, `--apply` renomeia em ordem cronológica, MOVE notas de subpastas `DIA N` e
|
|
21
|
+
da raiz para a pasta do mês, normaliza frontmatter/H1, reescreve wikilinks vault-wide
|
|
22
|
+
(full-path e basename) e remove pastas `DIA` vazias. Idempotente.
|
|
23
|
+
- Convenção injetada (VAULT_COMPLEMENT_RULES) e seeds wk-debugging (pt/en) ensinam a
|
|
24
|
+
numeração, a regra sem-DIA e o uso de `wendkeep note new`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `findLinkedDerivedNotes` (Stop hook) agora varre as pastas derivadas recursivamente —
|
|
29
|
+
antes só enxergava notas na raiz de 04-Decisões/05-Bugs/06-Aprendizados, então notas nas
|
|
30
|
+
subpastas de mês nunca entravam no merge de wikilinks da sessão.
|
|
31
|
+
|
|
32
|
+
### Migration
|
|
33
|
+
|
|
34
|
+
- Para migrar vaults existentes: `wendkeep renumber-bugs` (revisar preview) →
|
|
35
|
+
`wendkeep renumber-bugs --apply`; idem `renumber-learnings`. Depois
|
|
36
|
+
`wendkeep sync-defs --project . --reseed` para atualizar as skills wk-*.
|
|
37
|
+
|
|
38
|
+
## [0.40.0] — 2026-07-16
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `parseTasks` captura **todos** os `[req:]` de uma tarefa em `reqs: string[]` (`req` permanece
|
|
43
|
+
como alias do primeiro, retrocompatível). Antes, só o primeiro entrava no pacote de
|
|
44
|
+
verificação e os demais sumiam sem aviso.
|
|
45
|
+
- Heading de requisito aceita ID puro (`### Requisito: GATE-1`) como identidade, além do
|
|
46
|
+
formato preferido `### Requisito: <ID> — <nome>`. Diagnóstico de requisito órfão agora
|
|
47
|
+
ensina o formato esperado com exemplo concreto.
|
|
48
|
+
- `findProjectRoot`: `wendkeep verify` executado de um subdiretório sobe a árvore até achar
|
|
49
|
+
`wendkeep.sensors.json`/`.wendkeep.json` (à la `.git`); `--project` continua autoritativo.
|
|
50
|
+
- `--help`/`-h` universal: qualquer subcomando com `--help` imprime a ajuda e sai com 0,
|
|
51
|
+
interceptado antes da resolução de vault — nunca executa o comando.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Regex de ID de requisito unificada entre tarefa e spec (`REQ_ID_RE_SRC`): IDs
|
|
56
|
+
multi-segmento (`API-AUTH-2`) agora são reconhecidos também nas tarefas.
|
|
57
|
+
- `wendkeep verify` distingue `wendkeep.sensors.json` ausente (aviso com path + dica
|
|
58
|
+
`--project`) de JSON inválido (erro alto com a mensagem do parse). Antes, ambos viravam
|
|
59
|
+
"sensor não definido" para todos os sensores.
|
|
60
|
+
- `wendkeep import` com flag desconhecida agora falha com exit 2 citando a flag, em vez de
|
|
61
|
+
cair no default destrutivo `--source all` (que chegou a importar 78 sessões sem querer).
|
|
62
|
+
- Templates seed (skills de workflow pt/en) documentam o formato de heading de requisito e o
|
|
63
|
+
suporte a múltiplos `[req:]` por tarefa.
|
|
64
|
+
|
|
7
65
|
## [0.39.0] — 2026-07-13
|
|
8
66
|
|
|
9
67
|
### Added
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -68,6 +68,12 @@ Usage:
|
|
|
68
68
|
wendkeep renumber-decisions Renumber 04-Decisões to ADR-<NNNN>-<slug> in chronological order,
|
|
69
69
|
renaming files + rewriting every wikilink. Preview by default; --apply to
|
|
70
70
|
write. --vault P · --json.
|
|
71
|
+
wendkeep renumber-bugs Renumber 05-Bugs to BUG-<NNNN>-<slug> chronologically, moving notes
|
|
72
|
+
out of legacy "DIA N" subfolders into the month folder and rewriting
|
|
73
|
+
wikilinks. Preview by default; --apply · --vault P · --json.
|
|
74
|
+
wendkeep renumber-learnings Same for 06-Aprendizados/06-Learnings with APR-<NNNN>-<slug>.
|
|
75
|
+
wendkeep note new --type bug|learning "<título>" Create a numbered derived note (BUG-/APR-NNNN)
|
|
76
|
+
in the month folder and print its vault path. --date YYYY-MM-DD · --vault P.
|
|
71
77
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
72
78
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
73
79
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
@@ -125,6 +131,13 @@ async function preferProjectVault(argv) {
|
|
|
125
131
|
|
|
126
132
|
async function main() {
|
|
127
133
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
134
|
+
// Universal --help: any subcommand with --help/-h prints usage and never executes.
|
|
135
|
+
// Intercepted BEFORE vault resolution so it works anywhere — help must never depend
|
|
136
|
+
// on project state, and no command may treat --help as a runnable default.
|
|
137
|
+
if (cmd && (rest.includes('--help') || rest.includes('-h'))) {
|
|
138
|
+
process.stdout.write(HELP);
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
128
141
|
if (cmd && !['init', 'hook', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
129
142
|
await preferProjectVault(rest);
|
|
130
143
|
}
|
|
@@ -207,6 +220,21 @@ async function main() {
|
|
|
207
220
|
runRenumberDecisions(rest);
|
|
208
221
|
break;
|
|
209
222
|
}
|
|
223
|
+
case 'renumber-bugs': {
|
|
224
|
+
const { runRenumberBugs } = await import('../src/renumber.mjs');
|
|
225
|
+
runRenumberBugs(rest);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'renumber-learnings': {
|
|
229
|
+
const { runRenumberLearnings } = await import('../src/renumber.mjs');
|
|
230
|
+
runRenumberLearnings(rest);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'note': {
|
|
234
|
+
const { runNote } = await import('../src/note.mjs');
|
|
235
|
+
runNote(rest);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
210
238
|
case '--version':
|
|
211
239
|
case '-v':
|
|
212
240
|
process.stdout.write(`${version()}\n`);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
|
-
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline } from './spec-core.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline, REQ_ID_RE_SRC } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
10
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
@@ -161,18 +161,18 @@ export function parseTasks(md) {
|
|
|
161
161
|
const tasks = [];
|
|
162
162
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
163
163
|
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
164
|
-
const
|
|
164
|
+
const reqReG = new RegExp(`\\[req:\\s*(${REQ_ID_RE_SRC})\\]`, 'g');
|
|
165
165
|
let m;
|
|
166
166
|
while ((m = re.exec(String(md))) !== null) {
|
|
167
167
|
let text = m[3].trim();
|
|
168
168
|
const sm = text.match(sensorRe);
|
|
169
|
-
const
|
|
169
|
+
const reqs = [...text.matchAll(reqReG)].map((r) => r[1]);
|
|
170
170
|
const sensor = sm ? sm[1] : undefined;
|
|
171
|
-
const req = rm ? rm[1] : undefined;
|
|
172
171
|
if (sm) text = text.replace(sensorRe, '');
|
|
173
|
-
if (
|
|
172
|
+
if (reqs.length) text = text.replace(reqReG, '');
|
|
174
173
|
text = text.replace(/\s+/g, ' ').trim();
|
|
175
|
-
|
|
174
|
+
// `req` stays as alias of the first id — older consumers keep working.
|
|
175
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(reqs.length ? { req: reqs[0], reqs } : {}) });
|
|
176
176
|
}
|
|
177
177
|
return tasks;
|
|
178
178
|
}
|
|
@@ -419,7 +419,7 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
let reqIds = [];
|
|
422
|
-
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).
|
|
422
|
+
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).flatMap((t) => t.reqs ?? []))]; } catch { /* sem tarefas */ }
|
|
423
423
|
|
|
424
424
|
ensureDir(join(vaultBase, chDir, ARCHIVE_DIR));
|
|
425
425
|
try {
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
41
41
|
let tasks = [];
|
|
42
42
|
let tarefasMd = '';
|
|
43
43
|
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
|
|
44
|
-
const reqIds = [...new Set(tasks.
|
|
44
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
45
45
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
46
46
|
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
47
47
|
if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
derivedContentKey,
|
|
7
7
|
ensureDir,
|
|
8
8
|
getNextAdrNumber,
|
|
9
|
+
getNextDerivedNumber,
|
|
9
10
|
keysBate,
|
|
10
11
|
providerMeta,
|
|
11
12
|
slugify,
|
|
@@ -183,11 +184,13 @@ const NOTE_LABELS = {
|
|
|
183
184
|
};
|
|
184
185
|
function noteLabels(localeId) { return NOTE_LABELS[localeId] || NOTE_LABELS['pt-BR']; }
|
|
185
186
|
|
|
186
|
-
export function buildBugNoteContent(bug, issueRef, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(bug.rootCause), localeId = 'pt-BR') {
|
|
187
|
+
export function buildBugNoteContent(bug, issueRef, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(bug.rootCause), localeId = 'pt-BR', bugNum = 0) {
|
|
187
188
|
const L = noteLabels(localeId);
|
|
188
189
|
const title = issueRef
|
|
189
190
|
? `${issueRef} - ${normalizeInline(bug.rootCause, 80)}`
|
|
190
191
|
: normalizeInline(bug.rootCause, 80);
|
|
192
|
+
// bugNum = 0 keeps the legacy unnumbered shape (existing call sites/tests unaffected).
|
|
193
|
+
const heading = bugNum ? `# BUG-${String(bugNum).padStart(4, '0')} — ${title}` : `# Bug - ${title}`;
|
|
191
194
|
|
|
192
195
|
return `---
|
|
193
196
|
type: bug
|
|
@@ -195,7 +198,7 @@ date: ${dateStr}
|
|
|
195
198
|
status: fixed
|
|
196
199
|
provider: ${provider.id}
|
|
197
200
|
content_key: "${contentKey}"
|
|
198
|
-
${sessionYamlLinks(sessionRel)}
|
|
201
|
+
${bugNum ? `bug: ${bugNum}\n` : ''}${sessionYamlLinks(sessionRel)}
|
|
199
202
|
cssclasses:
|
|
200
203
|
- topic-bug
|
|
201
204
|
tags:
|
|
@@ -204,7 +207,7 @@ severity: ${yamlQuote(bug.severity)}
|
|
|
204
207
|
issue: ${yamlQuote(issueRef || '')}
|
|
205
208
|
---
|
|
206
209
|
|
|
207
|
-
|
|
210
|
+
${heading}
|
|
208
211
|
|
|
209
212
|
> [!note] ${L.autoTag}
|
|
210
213
|
> ${L.autoLine(provider.label)}
|
|
@@ -436,22 +439,24 @@ export function extractLearningDetails(tx, bugDetails) {
|
|
|
436
439
|
return learnings.length ? learnings.slice(0, 5) : null;
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title), localeId = 'pt-BR') {
|
|
442
|
+
export function buildLearningNoteContent(learning, dateStr, sessionRel, provider = providerMeta(), contentKey = derivedContentKey(learning.title), localeId = 'pt-BR', aprNum = 0) {
|
|
440
443
|
const L = noteLabels(localeId);
|
|
444
|
+
// aprNum = 0 keeps the legacy unnumbered shape (existing call sites/tests unaffected).
|
|
445
|
+
const heading = aprNum ? `# APR-${String(aprNum).padStart(4, '0')} — ${learning.title}` : `# ${L.learn.title} - ${learning.title}`;
|
|
441
446
|
return `---
|
|
442
447
|
type: learning
|
|
443
448
|
date: ${dateStr}
|
|
444
449
|
status: active
|
|
445
450
|
provider: ${provider.id}
|
|
446
451
|
content_key: "${contentKey}"
|
|
447
|
-
${sessionYamlLinks(sessionRel)}
|
|
452
|
+
${aprNum ? `apr: ${aprNum}\n` : ''}${sessionYamlLinks(sessionRel)}
|
|
448
453
|
cssclasses:
|
|
449
454
|
- topic-learning
|
|
450
455
|
tags:
|
|
451
456
|
${yamlTags(learning.tags.map((tag) => (tag === 'codex' ? provider.tag : tag)))}
|
|
452
457
|
---
|
|
453
458
|
|
|
454
|
-
|
|
459
|
+
${heading}
|
|
455
460
|
|
|
456
461
|
> [!note] ${L.autoTag}
|
|
457
462
|
> ${L.autoLine(provider.label)}
|
|
@@ -471,6 +476,81 @@ ${L.learn.futureHint}
|
|
|
471
476
|
`;
|
|
472
477
|
}
|
|
473
478
|
|
|
479
|
+
// Manual derived notes (`wendkeep note new`): same sections as the auto-generated shape,
|
|
480
|
+
// but with placeholders — the agent/human fills them in. status differs: a manual bug is
|
|
481
|
+
// OPEN (the auto one is extracted from an applied fix, hence fixed).
|
|
482
|
+
export function buildManualBugNote(title, { num, dateStr, sessionRel = '', localeId = 'pt-BR' }) {
|
|
483
|
+
const L = noteLabels(localeId);
|
|
484
|
+
const src = sessionRel ? `${sessionYamlLinks(sessionRel)}\n` : '';
|
|
485
|
+
return `---
|
|
486
|
+
type: bug
|
|
487
|
+
date: ${dateStr}
|
|
488
|
+
status: open
|
|
489
|
+
content_key: "${derivedContentKey(title)}"
|
|
490
|
+
bug: ${num}
|
|
491
|
+
${src}cssclasses:
|
|
492
|
+
- topic-bug
|
|
493
|
+
tags:
|
|
494
|
+
- bug
|
|
495
|
+
severity: ""
|
|
496
|
+
issue: ""
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
# BUG-${String(num).padStart(4, '0')} — ${title}
|
|
500
|
+
|
|
501
|
+
## ${L.bug.symptom}
|
|
502
|
+
|
|
503
|
+
${L.verify}
|
|
504
|
+
|
|
505
|
+
## ${L.bug.rootCause}
|
|
506
|
+
|
|
507
|
+
${L.verify}
|
|
508
|
+
|
|
509
|
+
## ${L.bug.fix}
|
|
510
|
+
|
|
511
|
+
${L.bug.noFix}
|
|
512
|
+
|
|
513
|
+
## ${L.bug.evidence}
|
|
514
|
+
|
|
515
|
+
${L.bug.addEvidence}
|
|
516
|
+
|
|
517
|
+
## ${L.bug.lessons}
|
|
518
|
+
|
|
519
|
+
${L.complete}
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function buildManualLearningNote(title, { num, dateStr, sessionRel = '', localeId = 'pt-BR' }) {
|
|
524
|
+
const L = noteLabels(localeId);
|
|
525
|
+
const src = sessionRel ? `${sessionYamlLinks(sessionRel)}\n` : '';
|
|
526
|
+
return `---
|
|
527
|
+
type: learning
|
|
528
|
+
date: ${dateStr}
|
|
529
|
+
status: active
|
|
530
|
+
content_key: "${derivedContentKey(title)}"
|
|
531
|
+
apr: ${num}
|
|
532
|
+
${src}cssclasses:
|
|
533
|
+
- topic-learning
|
|
534
|
+
tags:
|
|
535
|
+
- aprendizado
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
# APR-${String(num).padStart(4, '0')} — ${title}
|
|
539
|
+
|
|
540
|
+
## ${L.learn.context}
|
|
541
|
+
|
|
542
|
+
${L.complete}
|
|
543
|
+
|
|
544
|
+
## ${L.learn.learned}
|
|
545
|
+
|
|
546
|
+
${L.complete}
|
|
547
|
+
|
|
548
|
+
## ${L.learn.future}
|
|
549
|
+
|
|
550
|
+
${L.learn.futureHint}
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
|
|
474
554
|
const derivedFoldersFor = (vaultBase) => { const f = getLocale(vaultBase).folders; return { bugs: f.bugs, decisions: f.decisions, learnings: f.learnings }; };
|
|
475
555
|
|
|
476
556
|
function listMd(dir) {
|
|
@@ -544,9 +624,14 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
544
624
|
const bugKey = derivedContentKey(bugDetails.rootCause);
|
|
545
625
|
if (!alreadyHasKey(existingKeys.bugs, bugKey)) {
|
|
546
626
|
const causeSlug = slugify(bugDetails.rootCause, 'bug', 40);
|
|
547
|
-
|
|
627
|
+
// Numbered AFTER the dedup guard so a deduplicated note never burns a number.
|
|
628
|
+
const bugNum = getNextDerivedNumber(vaultBase, 'bugs', 'BUG');
|
|
629
|
+
// Keep the tracker ref in the name, but only once — root causes often repeat it.
|
|
630
|
+
const refSlug = issueRef ? slugify(issueRef, '', 20) : '';
|
|
631
|
+
const refPrefix = refSlug && !causeSlug.includes(refSlug) ? `${refSlug}-` : '';
|
|
632
|
+
const fileName = `BUG-${String(bugNum).padStart(4, '0')}-${refPrefix}${causeSlug}.md`;
|
|
548
633
|
const filePath = join(bugsDir, fileName);
|
|
549
|
-
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id), 'utf-8');
|
|
634
|
+
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id, bugNum), 'utf-8');
|
|
550
635
|
linked.bugs.push(toVaultRelative(vaultBase, filePath));
|
|
551
636
|
existingKeys.bugs.push(bugKey);
|
|
552
637
|
}
|
|
@@ -585,9 +670,11 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
585
670
|
if (alreadyHasKey(existingKeys.learnings, learningKey)) continue;
|
|
586
671
|
if (vaultLearningKeys.has(learningKey)) continue; // already learned elsewhere in the vault
|
|
587
672
|
const learningSlug = slugify(learning.title, 'aprendizado', 40);
|
|
588
|
-
|
|
673
|
+
// Minted inside the loop: each learning consumes its own sequential number.
|
|
674
|
+
const aprNum = getNextDerivedNumber(vaultBase, 'learnings', 'APR');
|
|
675
|
+
const fileName = `APR-${String(aprNum).padStart(4, '0')}-${learningSlug}.md`;
|
|
589
676
|
const filePath = join(learningsDir, fileName);
|
|
590
|
-
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id), 'utf-8');
|
|
677
|
+
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id, aprNum), 'utf-8');
|
|
591
678
|
linked.learnings.push(toVaultRelative(vaultBase, filePath));
|
|
592
679
|
existingKeys.learnings.push(learningKey);
|
|
593
680
|
}
|
|
@@ -19,6 +19,7 @@ export const VAULT_COMPLEMENT_RULES = [
|
|
|
19
19
|
'Regra prática do Vault: os hooks garantem o histórico automático por turno; o agente só complementa manualmente quando houver valor durável de memória, decisão, bug, aprendizado ou auditoria/validação.',
|
|
20
20
|
'Evite duplicar o que o hook já registra. Use escrita manual para síntese curada baseada em evidências, não para histórico bruto nem raciocínio interno.',
|
|
21
21
|
'Quando complementar, registre a síntese na sessão ativa dentro de `## Iterações` antes de `## Decisões geradas nesta sessão`, ou crie nota derivada em `04-Decisões/`, `05-Bugs/` ou `06-Aprendizados/` com backlink para a sessão.',
|
|
22
|
+
'Notas derivadas vivem na pasta do MÊS (`<pasta>/<ano>/<MM-MMM>/`), nunca em subpasta `DIA N`, com nome numerado `ADR-`/`BUG-`/`APR-NNNN-<slug>`. Para criar bug ou aprendizado manual, use `wendkeep note new --type bug|learning "título"` — o comando cria a nota já numerada no lugar certo e imprime o path; nunca escreva o arquivo à mão.',
|
|
22
23
|
'Atualize `SHARED_MEMORY.md` somente quando a síntese mudar estado ativo que outro agente precise saber.',
|
|
23
24
|
];
|
|
24
25
|
|
|
@@ -661,10 +662,12 @@ export function listMarkdownFiles(dir) {
|
|
|
661
662
|
}
|
|
662
663
|
}
|
|
663
664
|
|
|
664
|
-
|
|
665
|
-
|
|
665
|
+
// Sequential numbering shared by every derived-note family (ADR-, BUG-, APR-):
|
|
666
|
+
// recursive walk because notes live in dated subfolders (AAAA/MM-MMM and legacy DIA DD).
|
|
667
|
+
export function getNextDerivedNumber(vaultBase, folderKey, prefix) {
|
|
668
|
+
const baseDir = join(vaultBase, getLocale(vaultBase).folders[folderKey]);
|
|
669
|
+
const re = new RegExp(`^${prefix}-(\\d+)`, 'i');
|
|
666
670
|
let max = 0;
|
|
667
|
-
// Varre recursivamente: os ADRs agora vivem em subpastas datadas (AAAA/MM-MMM/DIA DD).
|
|
668
671
|
const walk = (dir) => {
|
|
669
672
|
let entries;
|
|
670
673
|
try {
|
|
@@ -676,15 +679,19 @@ export function getNextAdrNumber(vaultBase) {
|
|
|
676
679
|
if (entry.isDirectory()) {
|
|
677
680
|
walk(join(dir, entry.name));
|
|
678
681
|
} else {
|
|
679
|
-
const match = entry.name.match(
|
|
682
|
+
const match = entry.name.match(re);
|
|
680
683
|
if (match) max = Math.max(max, Number(match[1]));
|
|
681
684
|
}
|
|
682
685
|
}
|
|
683
686
|
};
|
|
684
|
-
walk(
|
|
687
|
+
walk(baseDir);
|
|
685
688
|
return max + 1;
|
|
686
689
|
}
|
|
687
690
|
|
|
691
|
+
export function getNextAdrNumber(vaultBase) {
|
|
692
|
+
return getNextDerivedNumber(vaultBase, 'decisions', 'ADR');
|
|
693
|
+
}
|
|
694
|
+
|
|
688
695
|
export function statExists(path) {
|
|
689
696
|
try {
|
|
690
697
|
return statSync(path);
|
|
@@ -102,7 +102,7 @@ export function rewriteLinks(content, renames) {
|
|
|
102
102
|
return c;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function allVaultMarkdown(vaultBase) {
|
|
105
|
+
export function allVaultMarkdown(vaultBase) {
|
|
106
106
|
const out = [];
|
|
107
107
|
const skip = new Set(['.git', '.obsidian', 'node_modules', '_arquivo']);
|
|
108
108
|
const walk = (dir) => {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Retroactive renumbering for the OTHER derived-note families (0.41.0): 05-Bugs -> BUG-NNNN,
|
|
3
|
+
// 06-Aprendizados/06-Learnings -> APR-NNNN. Mirrors renumber-decisions with one deliberate
|
|
4
|
+
// difference: the destination is always the MONTH folder of the note's resolved date — legacy
|
|
5
|
+
// `DIA N` subfolders and dated root notes are moved up/in, then empty `DIA *` dirs are removed.
|
|
6
|
+
// (Decisions renumber preserves dirname; the semantics differ, so the modules stay separate.)
|
|
7
|
+
// Known edge: an issueRef that itself starts with `BUG-\d+` would be eaten by the slug strip.
|
|
8
|
+
import { readdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join, dirname, relative } from 'node:path';
|
|
10
|
+
import { getLocale } from './locale.mjs';
|
|
11
|
+
import { ensureDir, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
12
|
+
import { padAdr, rewriteLinks, allVaultMarkdown } from './renumber-decisions.mjs';
|
|
13
|
+
|
|
14
|
+
export const DERIVED_KINDS = {
|
|
15
|
+
bugs: { folderKey: 'bugs', prefix: 'BUG', numField: 'bug', type: 'bug', fallbackSlug: 'bug' },
|
|
16
|
+
learnings: { folderKey: 'learnings', prefix: 'APR', numField: 'apr', type: 'learning', fallbackSlug: 'aprendizado' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function safeRead(abs) {
|
|
20
|
+
try { return readFileSync(abs, 'utf8'); } catch { return ''; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function walkNotes(vaultBase, folderRel) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const walk = (dir) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
const abs = join(dir, e.name);
|
|
30
|
+
if (e.isDirectory()) walk(abs);
|
|
31
|
+
else if (e.name.endsWith('.md')) {
|
|
32
|
+
out.push({ abs, rel: relative(vaultBase, abs).replaceAll('\\', '/'), base: e.name });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
walk(join(vaultBase, folderRel));
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve the note's date (frontmatter > filename prefix > `DIA N` folder > month folder).
|
|
41
|
+
// Returns 'YYYY-MM-DD' or '' when nothing is derivable.
|
|
42
|
+
export function derivedNoteDate({ abs, base, content }) {
|
|
43
|
+
const c = content ?? safeRead(abs);
|
|
44
|
+
const fmDate = c.match(/^date:\s*(\d{4}-\d{2}-\d{2})/m);
|
|
45
|
+
if (fmDate) return fmDate[1];
|
|
46
|
+
const fnDate = base.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
47
|
+
if (fnDate) return fnDate[1];
|
|
48
|
+
const posix = abs.replaceAll('\\', '/');
|
|
49
|
+
const dayFolder = posix.match(/\/(\d{4})\/(\d{2})-[^/]+\/DIA\s+(\d{1,2})\//i);
|
|
50
|
+
if (dayFolder) return `${dayFolder[1]}-${dayFolder[2]}-${String(dayFolder[3]).padStart(2, '0')}`;
|
|
51
|
+
const monthFolder = posix.match(/\/(\d{4})\/(\d{2})-[^/]+\//);
|
|
52
|
+
if (monthFolder) return `${monthFolder[1]}-${monthFolder[2]}-01`;
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function derivedSortKey(note, prefix) {
|
|
57
|
+
const date = derivedNoteDate(note) || '9999-12-31';
|
|
58
|
+
const numMatch = note.base.match(new RegExp(`^${prefix}-(\\d+)`, 'i'));
|
|
59
|
+
const num = numMatch ? Number(numMatch[1]) : 999999;
|
|
60
|
+
return `${date}#${String(num).padStart(6, '0')}#${note.base}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Descriptive slug from any era's filename: PREFIX-NNNN-, date prefix and legacy `bug-` marker out.
|
|
64
|
+
export function slugFromDerivedName(base, kind) {
|
|
65
|
+
let s = base.replace(/\.md$/i, '');
|
|
66
|
+
s = s.replace(new RegExp(`^${kind.prefix}-\\d+-`, 'i'), '');
|
|
67
|
+
s = s.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
68
|
+
if (kind.folderKey === 'bugs') s = s.replace(/^bug-/i, '');
|
|
69
|
+
return s || kind.fallbackSlug;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Normalize body: type, numeric field (bug:/apr:) and the canonical `PREFIX-NNNN — ` H1.
|
|
73
|
+
export function normalizeDerivedContent(content, num, kind) {
|
|
74
|
+
let c = String(content || '');
|
|
75
|
+
const label = `${kind.prefix}-${padAdr(num)}`;
|
|
76
|
+
|
|
77
|
+
if (/^type:\s*.*$/m.test(c)) c = c.replace(/^type:\s*.*$/m, `type: ${kind.type}`);
|
|
78
|
+
else c = c.replace(/^---\n/, `---\ntype: ${kind.type}\n`);
|
|
79
|
+
|
|
80
|
+
const fieldRe = new RegExp(`^${kind.numField}:\\s*.*$`, 'm');
|
|
81
|
+
if (fieldRe.test(c)) c = c.replace(fieldRe, `${kind.numField}: ${num}`);
|
|
82
|
+
else c = c.replace(new RegExp(`^type: ${kind.type}$`, 'm'), `type: ${kind.type}\n${kind.numField}: ${num}`);
|
|
83
|
+
|
|
84
|
+
c = c.replace(/^#\s+(.*)$/m, (_, title) => {
|
|
85
|
+
let bare = title.replace(new RegExp(`^${kind.prefix}-\\d+\\s*[—–-]\\s*`, 'i'), '');
|
|
86
|
+
bare = bare.replace(/^(?:Bug|Aprendizado|Learning)\s*[-—–]\s*/i, '').trim();
|
|
87
|
+
return `# ${label} — ${bare}`;
|
|
88
|
+
});
|
|
89
|
+
return c;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pure plan: chronological numbering, destination = month folder of the resolved date
|
|
93
|
+
// (fallback: keep the current dirname when no date is derivable).
|
|
94
|
+
export function planRenumberDerived(vaultBase, kindId) {
|
|
95
|
+
const kind = DERIVED_KINDS[kindId];
|
|
96
|
+
if (!kind) throw new Error(`renumber-derived: unknown kind "${kindId}"`);
|
|
97
|
+
const folderRel = getLocale(vaultBase).folders[kind.folderKey];
|
|
98
|
+
const notes = walkNotes(vaultBase, folderRel)
|
|
99
|
+
.map((n) => ({ ...n, content: safeRead(n.abs) }))
|
|
100
|
+
.sort((a, b) => derivedSortKey(a, kind.prefix).localeCompare(derivedSortKey(b, kind.prefix)));
|
|
101
|
+
|
|
102
|
+
const renames = [];
|
|
103
|
+
notes.forEach((n, i) => {
|
|
104
|
+
const num = i + 1;
|
|
105
|
+
const slug = slugFromDerivedName(n.base, kind);
|
|
106
|
+
const newBase = `${kind.prefix}-${padAdr(num)}-${slug}.md`;
|
|
107
|
+
const date = derivedNoteDate(n);
|
|
108
|
+
const destDirRel = date ? monthFolderRelFromDateStr(folderRel, date, vaultBase) : dirname(n.rel);
|
|
109
|
+
const newRel = `${destDirRel.replaceAll('\\', '/')}/${newBase}`;
|
|
110
|
+
renames.push({
|
|
111
|
+
num, slug,
|
|
112
|
+
oldAbs: n.abs,
|
|
113
|
+
newAbs: join(vaultBase, destDirRel, newBase),
|
|
114
|
+
oldRelNoExt: n.rel.replace(/\.md$/i, ''),
|
|
115
|
+
newRelNoExt: newRel.replace(/\.md$/i, ''),
|
|
116
|
+
oldBaseNoExt: n.base.replace(/\.md$/i, ''),
|
|
117
|
+
newBaseNoExt: newBase.replace(/\.md$/i, ''),
|
|
118
|
+
renamed: n.rel !== newRel,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
return renames;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Remove now-empty `DIA *` folders under the derived root (best-effort, fail-quiet).
|
|
125
|
+
function pruneEmptyDayFolders(vaultBase, folderRel) {
|
|
126
|
+
const walk = (dir) => {
|
|
127
|
+
let entries;
|
|
128
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
if (!e.isDirectory()) continue;
|
|
131
|
+
const abs = join(dir, e.name);
|
|
132
|
+
walk(abs);
|
|
133
|
+
if (/^DIA\s/i.test(e.name)) {
|
|
134
|
+
try { rmdirSync(abs); } catch { /* não-vazio — fica */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
walk(join(vaultBase, folderRel));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renumberDerived(vaultBase, kindId, { apply = false } = {}) {
|
|
142
|
+
const kind = DERIVED_KINDS[kindId];
|
|
143
|
+
const renames = planRenumberDerived(vaultBase, kindId);
|
|
144
|
+
const changed = renames.filter((r) => r.renamed);
|
|
145
|
+
const report = {
|
|
146
|
+
total: renames.length,
|
|
147
|
+
renamed: changed.length,
|
|
148
|
+
normalized: 0,
|
|
149
|
+
linksUpdated: 0,
|
|
150
|
+
filesTouched: 0,
|
|
151
|
+
plan: renames.map((r) => ({ num: r.num, from: r.oldRelNoExt, to: r.newRelNoExt, renamed: r.renamed })),
|
|
152
|
+
applied: apply,
|
|
153
|
+
};
|
|
154
|
+
if (!apply) return report;
|
|
155
|
+
|
|
156
|
+
// Phase A — park renamed sources under temp names (no target clobbers an unmoved source).
|
|
157
|
+
const temps = new Map();
|
|
158
|
+
changed.forEach((r, i) => {
|
|
159
|
+
const tmp = join(dirname(r.oldAbs), `.wk-renum-${i}.tmp`);
|
|
160
|
+
renameSync(r.oldAbs, tmp);
|
|
161
|
+
temps.set(r, tmp);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Phase B — normalize body and land at the final path (possibly a different directory).
|
|
165
|
+
for (const r of renames) {
|
|
166
|
+
const tmp = temps.get(r);
|
|
167
|
+
ensureDir(dirname(r.newAbs));
|
|
168
|
+
if (tmp) {
|
|
169
|
+
writeFileSync(tmp, normalizeDerivedContent(safeRead(tmp), r.num, kind), 'utf8');
|
|
170
|
+
renameSync(tmp, r.newAbs);
|
|
171
|
+
} else {
|
|
172
|
+
writeFileSync(r.newAbs, normalizeDerivedContent(safeRead(r.oldAbs), r.num, kind), 'utf8');
|
|
173
|
+
}
|
|
174
|
+
report.normalized += 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Phase C — rewrite wikilinks vault-wide (full-path and basename forms).
|
|
178
|
+
for (const abs of allVaultMarkdown(vaultBase)) {
|
|
179
|
+
const before = safeRead(abs);
|
|
180
|
+
const after = rewriteLinks(before, changed);
|
|
181
|
+
if (after !== before) { writeFileSync(abs, after, 'utf8'); report.filesTouched += 1; report.linksUpdated += 1; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Phase D — drop empty legacy `DIA *` folders.
|
|
185
|
+
pruneEmptyDayFolders(vaultBase, getLocale(vaultBase).folders[kind.folderKey]);
|
|
186
|
+
return report;
|
|
187
|
+
}
|
package/hooks/sensors-core.mjs
CHANGED
|
@@ -2,15 +2,37 @@
|
|
|
2
2
|
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
3
|
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
9
|
+
return loadSensorsDetailed(projectRoot, file).sensors;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Missing config and broken config are different failures: absent file usually means
|
|
13
|
+
// wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
|
|
14
|
+
// both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
|
|
15
|
+
export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
|
|
16
|
+
const path = join(projectRoot, file);
|
|
17
|
+
if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
|
|
9
18
|
try {
|
|
10
|
-
const data = JSON.parse(readFileSync(
|
|
11
|
-
return Array.isArray(data.sensors) ? data.sensors : [];
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
19
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { sensors: [], missing: false, error: e.message, path };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Climb the directory tree looking for a project marker (wendkeep.sensors.json or
|
|
27
|
+
// .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
|
|
28
|
+
// across commands, so verify is often run from a subdirectory.
|
|
29
|
+
export function findProjectRoot(startDir) {
|
|
30
|
+
let dir = resolve(startDir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) return null;
|
|
35
|
+
dir = parent;
|
|
14
36
|
}
|
|
15
37
|
}
|
|
16
38
|
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { request } from 'http';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
@@ -803,10 +803,15 @@ export function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
|
803
803
|
learnings: locF.learnings,
|
|
804
804
|
};
|
|
805
805
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
806
|
+
// Recursive: derived notes live in month subfolders (04-Decisões/2026/07-JUL/ADR-...,
|
|
807
|
+
// 05-Bugs/.../BUG-...) — a root-only scan missed every one of them.
|
|
808
|
+
const walk = (dir, key) => {
|
|
809
|
+
let entries;
|
|
810
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
811
|
+
for (const entry of entries) {
|
|
812
|
+
const absPath = join(dir, entry.name);
|
|
813
|
+
if (entry.isDirectory()) { walk(absPath, key); continue; }
|
|
814
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
810
815
|
try {
|
|
811
816
|
const content = readFileSync(absPath, 'utf-8');
|
|
812
817
|
if (noteReferencesSession(content, sessionRel)) {
|
|
@@ -816,7 +821,8 @@ export function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
|
816
821
|
// Ignore unreadable notes; the hook must not block session shutdown.
|
|
817
822
|
}
|
|
818
823
|
}
|
|
819
|
-
}
|
|
824
|
+
};
|
|
825
|
+
for (const [key, folder] of Object.entries(folders)) walk(join(vaultBase, folder), key);
|
|
820
826
|
|
|
821
827
|
return linked;
|
|
822
828
|
}
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -22,15 +22,29 @@ export const MANAGED_SPEC_MARKER = '<!-- wendkeep:managed-spec — generated fro
|
|
|
22
22
|
// Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
|
|
23
23
|
const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
|
|
24
24
|
|
|
25
|
+
// Single source of truth for requirement-id shape — task tags ([req:ID]) and spec
|
|
26
|
+
// headings MUST agree, or coverage silently misses (multi-segment ids like API-AUTH-2).
|
|
27
|
+
export const REQ_ID_RE_SRC = '[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\\d+';
|
|
28
|
+
|
|
29
|
+
// Orphan diagnostics must teach the fix, not just name the ids — the heading format
|
|
30
|
+
// is the most common cause and lives only here.
|
|
31
|
+
export function formatOrphanReqs(ids) {
|
|
32
|
+
const list = ids.join(', ');
|
|
33
|
+
const ex = ids[0] || 'GATE-1';
|
|
34
|
+
return `requisito(s) órfão(s) na spec efetiva: ${list} — heading esperado no spec.md da change: "### Requisito: ${ex} — <nome>" (ou só "### Requisito: ${ex}")`;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export function parseRequirements(md) {
|
|
26
38
|
const text = String(md);
|
|
27
39
|
const matches = [...text.matchAll(REQ_RE)];
|
|
28
40
|
const reqs = [];
|
|
29
41
|
for (let i = 0; i < matches.length; i += 1) {
|
|
30
42
|
const raw = matches[i][1].trim();
|
|
31
|
-
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"
|
|
32
|
-
|
|
33
|
-
const
|
|
43
|
+
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>" or a bare
|
|
44
|
+
// "<ID>"; else the whole text. Bare ids keep specs writable without the em-dash.
|
|
45
|
+
const idM = raw.match(new RegExp(`^(${REQ_ID_RE_SRC})\\s*—\\s*(.+)$`));
|
|
46
|
+
const bare = idM ? null : raw.match(new RegExp(`^(${REQ_ID_RE_SRC})$`));
|
|
47
|
+
const id = idM ? idM[1] : bare ? bare[1] : null;
|
|
34
48
|
const name = idM ? idM[2].trim() : raw;
|
|
35
49
|
const start = matches[i].index + matches[i][0].length;
|
|
36
50
|
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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": {
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"wendkeep": "^0.
|
|
49
|
+
"wendkeep": "^0.39.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/change.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
scaffoldPlaceholders,
|
|
18
18
|
} from '../hooks/change-core.mjs';
|
|
19
19
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
20
|
-
import { buildEffectiveRequirementPackage, evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
20
|
+
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
21
21
|
import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
22
22
|
import { getLocale } from '../hooks/locale.mjs';
|
|
23
23
|
|
|
@@ -147,13 +147,13 @@ export function runChange(argv) {
|
|
|
147
147
|
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
148
148
|
process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
|
|
149
149
|
for (const t of tasks) {
|
|
150
|
-
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.
|
|
150
|
+
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${(t.reqs ?? []).map((r) => ` [req:${r}]`).join('')}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
|
|
151
151
|
}
|
|
152
152
|
let evidence = null;
|
|
153
153
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
154
154
|
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
155
155
|
else process.stdout.write('evidencia: ausente\n');
|
|
156
|
-
const reqIds = [...new Set(tasks.
|
|
156
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
157
157
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
158
158
|
if (effective.errors.length || effective.missing.length) {
|
|
159
159
|
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
@@ -239,10 +239,10 @@ export function runChange(argv) {
|
|
|
239
239
|
return { ok: false, failing: ['evidência stale (tarefas.md mudou desde o último verify) — rode `wendkeep verify` de novo'] };
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
const reqIds = [...new Set(tasks.
|
|
242
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
243
243
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
244
244
|
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
245
|
-
if (effective.missing.length) return { ok: false, failing: [
|
|
245
|
+
if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
|
|
246
246
|
let evidence = [];
|
|
247
247
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
248
248
|
const s = evaluateGate(evidence, required);
|
package/src/import.mjs
CHANGED
|
@@ -12,7 +12,22 @@ function opt(argv, name) {
|
|
|
12
12
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const KNOWN_FLAGS = new Set([
|
|
16
|
+
'--vault', '--project', '--source', '--from', '--codex-from', '--since', '--limit',
|
|
17
|
+
'--dry-run', '--json', '--rescan-decisions', '--stamp-ids', '--help', '-h',
|
|
18
|
+
]);
|
|
19
|
+
|
|
15
20
|
export function runImportCli(argv) {
|
|
21
|
+
// Import writes to the vault; an unrecognized flag must never fall through to the
|
|
22
|
+
// destructive default (--source all). Fail fast, point at --help.
|
|
23
|
+
for (const a of argv) {
|
|
24
|
+
if (!a.startsWith('-')) continue;
|
|
25
|
+
const name = a.includes('=') ? a.slice(0, a.indexOf('=')) : a;
|
|
26
|
+
if (!KNOWN_FLAGS.has(name)) {
|
|
27
|
+
process.stderr.write(`wendkeep import: flag desconhecida "${name}" (use --help)\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
17
32
|
if (!vaultRaw) { process.stderr.write('wendkeep import: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
18
33
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
package/src/note.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `wendkeep note new` — manual derived notes without guesswork. The agent (or a human)
|
|
2
|
+
// asks for a bug/learning note and gets it already numbered (BUG-/APR-NNNN), in the month
|
|
3
|
+
// folder of the right derived tree, with the correct frontmatter — and the created vault
|
|
4
|
+
// path on stdout. Nobody computes the next number by hand, nobody recreates `DIA N` folders.
|
|
5
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, resolve, join, dirname } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
ensureDir,
|
|
9
|
+
getNextDerivedNumber,
|
|
10
|
+
monthFolderRelFromDateStr,
|
|
11
|
+
readControl,
|
|
12
|
+
slugify,
|
|
13
|
+
uniquePath,
|
|
14
|
+
toVaultRelative,
|
|
15
|
+
} from '../hooks/obsidian-common.mjs';
|
|
16
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
17
|
+
import { buildManualBugNote, buildManualLearningNote } from '../hooks/linked-notes.mjs';
|
|
18
|
+
|
|
19
|
+
const TYPES = {
|
|
20
|
+
bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
|
|
21
|
+
learning: { folderKey: 'learnings', prefix: 'APR', build: buildManualLearningNote },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function opt(argv, name) {
|
|
25
|
+
const i = argv.indexOf(name);
|
|
26
|
+
if (i >= 0) return argv[i + 1];
|
|
27
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
28
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runNote(argv) {
|
|
32
|
+
const [sub, ...rest] = argv;
|
|
33
|
+
if (sub !== 'new') {
|
|
34
|
+
process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"`).\n');
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const type = (opt(rest, '--type') || '').toLowerCase();
|
|
39
|
+
const kind = TYPES[type];
|
|
40
|
+
if (!kind) {
|
|
41
|
+
process.stderr.write('wendkeep note new: --type deve ser bug ou learning.\n');
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Título = primeiro argumento posicional (não-flag, não-valor de flag).
|
|
46
|
+
const flagValues = new Set([opt(rest, '--type'), opt(rest, '--vault'), opt(rest, '--date')].filter(Boolean));
|
|
47
|
+
const title = rest.find((a) => !a.startsWith('--') && !flagValues.has(a));
|
|
48
|
+
if (!title || !title.trim()) {
|
|
49
|
+
process.stderr.write('wendkeep note new: falta o título — `note new --type bug "resumo do bug"`.\n');
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dateStr = opt(rest, '--date') || new Date().toISOString().slice(0, 10);
|
|
54
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
55
|
+
process.stderr.write(`wendkeep note new: --date inválida "${dateStr}" (use YYYY-MM-DD).\n`);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
60
|
+
if (!vaultRaw) { process.stderr.write('wendkeep note new: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
61
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
62
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep note new: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
63
|
+
|
|
64
|
+
const loc = getLocale(vaultBase);
|
|
65
|
+
const num = getNextDerivedNumber(vaultBase, kind.folderKey, kind.prefix);
|
|
66
|
+
const dirRel = monthFolderRelFromDateStr(loc.folders[kind.folderKey], dateStr, vaultBase);
|
|
67
|
+
const fileName = `${kind.prefix}-${String(num).padStart(4, '0')}-${slugify(title, type, 60)}.md`;
|
|
68
|
+
const filePath = uniquePath(join(vaultBase, dirRel, fileName));
|
|
69
|
+
ensureDir(dirname(filePath));
|
|
70
|
+
|
|
71
|
+
let sessionRel = '';
|
|
72
|
+
try {
|
|
73
|
+
const control = readControl(vaultBase);
|
|
74
|
+
if (control?.status === 'active' && control.session_file) sessionRel = control.session_file;
|
|
75
|
+
} catch { /* sem sessão ativa — nota nasce sem backlink */ }
|
|
76
|
+
|
|
77
|
+
writeFileSync(filePath, kind.build(title.trim(), { num, dateStr, sessionRel, localeId: loc.id }), 'utf8');
|
|
78
|
+
process.stdout.write(`${toVaultRelative(vaultBase, filePath)}\n`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
package/src/renumber.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// `wendkeep renumber-decisions` — retroactive
|
|
2
|
-
// `ADR-<NNNN>-<slug>` in chronological order, renames the
|
|
3
|
-
//
|
|
1
|
+
// `wendkeep renumber-decisions|-bugs|-learnings` — retroactive renumbering. Renumbers every note
|
|
2
|
+
// in the derived folder (`ADR-`/`BUG-`/`APR-<NNNN>-<slug>`) in chronological order, renames the
|
|
3
|
+
// files (bugs/learnings also move out of legacy `DIA N` folders into the month folder), and
|
|
4
|
+
// rewrites every wikilink to them across the vault. Preview by default; --apply writes. Idempotent.
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import { isAbsolute, resolve } from 'node:path';
|
|
6
7
|
import { renumberDecisions } from '../hooks/renumber-decisions.mjs';
|
|
8
|
+
import { renumberDerived, DERIVED_KINDS } from '../hooks/renumber-derived.mjs';
|
|
7
9
|
|
|
8
10
|
function opt(argv, name) {
|
|
9
11
|
const i = argv.indexOf(name);
|
|
@@ -12,21 +14,33 @@ function opt(argv, name) {
|
|
|
12
14
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
function runRenumberCli(argv, cmdName, unitLabel, prefix, run) {
|
|
16
18
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
17
|
-
if (!vaultRaw) { process.stderr.write(
|
|
19
|
+
if (!vaultRaw) { process.stderr.write(`wendkeep ${cmdName}: no vault (--vault or OBSIDIAN_VAULT_PATH).\n`); process.exit(2); }
|
|
18
20
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
19
|
-
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep
|
|
21
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep ${cmdName}: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
20
22
|
|
|
21
23
|
const apply = argv.includes('--apply');
|
|
22
|
-
const report =
|
|
24
|
+
const report = run(vaultBase, { apply });
|
|
23
25
|
|
|
24
26
|
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exit(0); }
|
|
25
27
|
|
|
26
|
-
process.stdout.write(`${report.total}
|
|
28
|
+
process.stdout.write(`${report.total} ${unitLabel} · ${report.renamed} a renomear${apply ? ` · ${report.filesTouched} arquivo(s) com links atualizados` : ''}\n`);
|
|
27
29
|
for (const p of report.plan.filter((x) => x.renamed)) {
|
|
28
|
-
process.stdout.write(`
|
|
30
|
+
process.stdout.write(` ${prefix}-${String(p.num).padStart(4, '0')} ${p.from}\n → ${p.to}\n`);
|
|
29
31
|
}
|
|
30
32
|
if (!apply) process.stdout.write('\nNada foi escrito (preview). Rode com --apply para renomear e atualizar os wikilinks.\n');
|
|
31
33
|
process.exit(0);
|
|
32
34
|
}
|
|
35
|
+
|
|
36
|
+
export function runRenumberDecisions(argv) {
|
|
37
|
+
runRenumberCli(argv, 'renumber-decisions', 'decisão(ões)', 'ADR', (vault, o) => renumberDecisions(vault, o));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function runRenumberBugs(argv) {
|
|
41
|
+
runRenumberCli(argv, 'renumber-bugs', 'bug(s)', DERIVED_KINDS.bugs.prefix, (vault, o) => renumberDerived(vault, 'bugs', o));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runRenumberLearnings(argv) {
|
|
45
|
+
runRenumberCli(argv, 'renumber-learnings', 'aprendizado(s)', DERIVED_KINDS.learnings.prefix, (vault, o) => renumberDerived(vault, 'learnings', o));
|
|
46
|
+
}
|
package/src/skills-seed.mjs
CHANGED
|
@@ -35,15 +35,18 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
35
35
|
Antes de implementar, resolva \`spec_impact\` na proposta:
|
|
36
36
|
- \`required\`: liste a capability em \`specs:\` e preencha
|
|
37
37
|
\`specs/<capability>/spec.md\` com ADDED/MODIFIED/REMOVED; ligue tarefas com \`[req:ID]\`.
|
|
38
|
+
Heading de requisito: \`### Requisito: <ID> — <nome>\` (ou só \`### Requisito: <ID>\`);
|
|
39
|
+
o ID é a identidade (ex.: \`GATE-1\`, \`API-AUTH-2\`).
|
|
38
40
|
- \`none\`: registre uma justificativa real em \`spec_impact_reason\`.
|
|
39
41
|
\`pending\` nunca é estado pronto para implementação ou archive.
|
|
40
42
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
41
43
|
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
42
44
|
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
43
45
|
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
44
|
-
quando a change mexe numa capability.
|
|
46
|
+
quando a change mexe numa capability. Uma tarefa pode declarar vários
|
|
47
|
+
\`[req:]\` — todos contam na cobertura. Toda autoria de spec ocorre somente em
|
|
45
48
|
\`08-Mudanças/<slug>/specs/<capability>/spec.md\`; \`07-Specs\` é gerado/read-only.
|
|
46
|
-
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
49
|
+
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [req:MEM-2] [sensor:memory-validation]\`.
|
|
47
50
|
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
48
51
|
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
49
52
|
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
@@ -62,6 +65,9 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
62
65
|
o que você declarou. Sem \`[sensor:]\`, o archive não trava.
|
|
63
66
|
- A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
|
|
64
67
|
o grafo do Obsidian mostra plano↔sessão↔decisão.
|
|
68
|
+
- Notas derivadas (bug/aprendizado) são numeradas (\`BUG-NNNN-\`/\`APR-NNNN-\`) e vivem na
|
|
69
|
+
pasta do mês — nunca em subpasta \`DIA N\`. Crie via \`wendkeep note new --type
|
|
70
|
+
bug|learning "título"\` (imprime o path), não à mão.
|
|
65
71
|
`;
|
|
66
72
|
|
|
67
73
|
const TDD = `# TDD — Red, Green, Refactor (testes que discriminam)
|
|
@@ -129,6 +135,13 @@ mudando coisas no escuro.
|
|
|
129
135
|
isole uma variável por vez.
|
|
130
136
|
- Leia a mensagem de erro inteira e a stack — a linha decisiva costuma estar ali.
|
|
131
137
|
- "Não faz sentido" = uma suposição sua está errada. Cheque as suposições, não o improvável.
|
|
138
|
+
|
|
139
|
+
## Registro no vault
|
|
140
|
+
|
|
141
|
+
Bug com valor durável vira nota numerada: \`wendkeep note new --type bug "resumo"\` —
|
|
142
|
+
cria \`BUG-NNNN-<slug>.md\` na pasta do mês de \`05-Bugs\` (nunca subpasta \`DIA N\`) e
|
|
143
|
+
imprime o path pra você preencher sintoma/causa raiz/correção. Aprendizado extraído do
|
|
144
|
+
debug: \`wendkeep note new --type learning "lição"\` (vira \`APR-NNNN-\` em \`06-Aprendizados\`).
|
|
132
145
|
`;
|
|
133
146
|
|
|
134
147
|
const BRAINSTORMING = `# Brainstorming — ideia vira design
|
|
@@ -259,8 +272,11 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
259
272
|
Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
|
|
260
273
|
\`specs:\` plus a real \`specs/<capability>/spec.md\` delta and \`[req:ID]\` links; \`none\`
|
|
261
274
|
needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
|
|
275
|
+
Requirement heading: \`### Requirement: <ID> — <name>\` (or bare \`### Requirement: <ID>\`);
|
|
276
|
+
the ID is the identity (e.g. \`GATE-1\`, \`API-AUTH-2\`).
|
|
262
277
|
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
263
|
-
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies
|
|
278
|
+
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies;
|
|
279
|
+
a task may declare several \`[req:]\` tags — all of them count toward coverage).
|
|
264
280
|
Author specs only in \`08-Changes/<slug>/specs/\`; \`07-Specs\` is generated/read-only.
|
|
265
281
|
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
266
282
|
the verification package.
|
|
@@ -275,6 +291,9 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
275
291
|
\`wendkeep change use <slug>\` or \`--change <slug>\` where available.
|
|
276
292
|
- No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
|
|
277
293
|
- The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
|
|
294
|
+
- Derived notes (bug/learning) are numbered (\`BUG-NNNN-\`/\`APR-NNNN-\`) and live in the
|
|
295
|
+
month folder — never in a \`DIA N\` subfolder. Create them via \`wendkeep note new --type
|
|
296
|
+
bug|learning "title"\` (prints the path), never by hand.
|
|
278
297
|
`;
|
|
279
298
|
|
|
280
299
|
const TDD_EN = `# TDD — Red, Green, Refactor (tests that discriminate)
|
|
@@ -318,6 +337,13 @@ Use it when something fails or behaves wrong. One hypothesis at a time — no sh
|
|
|
318
337
|
|
|
319
338
|
Changed several things and it "worked"? You don't know what fixed it — revert, isolate one
|
|
320
339
|
variable. Read the whole error + stack — the decisive line is usually there.
|
|
340
|
+
|
|
341
|
+
## Vault record
|
|
342
|
+
|
|
343
|
+
A durable bug becomes a numbered note: \`wendkeep note new --type bug "summary"\` — creates
|
|
344
|
+
\`BUG-NNNN-<slug>.md\` in the month folder of \`05-Bugs\` (never a \`DIA N\` subfolder) and
|
|
345
|
+
prints the path for you to fill in. A learning from the debug: \`wendkeep note new --type
|
|
346
|
+
learning "lesson"\` (becomes \`APR-NNNN-\` in the learnings folder).
|
|
321
347
|
`;
|
|
322
348
|
|
|
323
349
|
const BRAINSTORMING_EN = `# Brainstorming — idea into design
|
package/src/spec.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function runSpec(argv) {
|
|
|
49
49
|
tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
|
|
50
50
|
}
|
|
51
51
|
catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
|
|
52
|
-
const reqIds = [...new Set(tasks.
|
|
52
|
+
const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
|
|
53
53
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
54
54
|
if (effective.errors.length) {
|
|
55
55
|
process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
|
package/src/taxonomy.mjs
CHANGED
package/src/verify.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { loadSensorsDetailed, findProjectRoot, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
import {
|
|
9
9
|
buildEffectiveRequirementPackage,
|
|
10
10
|
captureSpecBaseline,
|
|
11
|
+
formatOrphanReqs,
|
|
11
12
|
tasksHashOf,
|
|
12
13
|
} from '../hooks/spec-core.mjs';
|
|
13
14
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
@@ -29,7 +30,9 @@ export function runVerify(argv) {
|
|
|
29
30
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
30
31
|
if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
31
32
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
32
|
-
|
|
33
|
+
// --project wins; otherwise climb from cwd to the nearest project marker (agent shells
|
|
34
|
+
// keep their cwd across commands, so verify from a subdirectory is a recurring miss).
|
|
35
|
+
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
33
36
|
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
34
37
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
35
38
|
|
|
@@ -39,7 +42,15 @@ export function runVerify(argv) {
|
|
|
39
42
|
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|
|
40
43
|
|
|
41
44
|
const ids = requiredSensors(parseTasks(tarefas));
|
|
42
|
-
const
|
|
45
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
46
|
+
if (loaded.error) {
|
|
47
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json inválido em ${loaded.path}: ${loaded.error}\n`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
if (loaded.missing && ids.length) {
|
|
51
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
|
|
52
|
+
}
|
|
53
|
+
const sensors = loaded.sensors;
|
|
43
54
|
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
44
55
|
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
45
56
|
// Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
|
|
@@ -90,7 +101,7 @@ export function runVerify(argv) {
|
|
|
90
101
|
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
91
102
|
if (argv.includes('--deep')) {
|
|
92
103
|
const tasks = parseTasks(tarefas);
|
|
93
|
-
const reqIds = [...new Set(tasks.
|
|
104
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
94
105
|
const tasksHash = tasksHashOf(tarefas);
|
|
95
106
|
captureSpecBaseline(vaultBase, changeDir);
|
|
96
107
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
@@ -99,7 +110,7 @@ export function runVerify(argv) {
|
|
|
99
110
|
process.exit(1);
|
|
100
111
|
}
|
|
101
112
|
if (effective.missing.length) {
|
|
102
|
-
process.stderr.write(`verify --deep:
|
|
113
|
+
process.stderr.write(`verify --deep: ${formatOrphanReqs(effective.missing)}\n`);
|
|
103
114
|
process.exit(1);
|
|
104
115
|
}
|
|
105
116
|
const pkg = {
|
|
@@ -116,7 +127,7 @@ export function runVerify(argv) {
|
|
|
116
127
|
body: req.body,
|
|
117
128
|
};
|
|
118
129
|
}),
|
|
119
|
-
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
130
|
+
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
|
|
120
131
|
sensors: evidence,
|
|
121
132
|
};
|
|
122
133
|
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|