wendkeep 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ 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.42.0] — 2026-07-17
8
+
9
+ ### Changed
10
+
11
+ - `wendkeep renumber-decisions --apply` agora **move** as notas de subpastas `DIA N` para a
12
+ pasta do mês da sua data (achatando o legado), consistente com `renumber-bugs`/
13
+ `renumber-learnings` do 0.41.0. Antes só renomeava in-place para `ADR-NNNN-<slug>` e
14
+ deixava as pastas de dia intactas. Os wikilinks já eram reescritos vault-wide; agora
15
+ acompanham o novo caminho de mês, e as pastas `DIA` que ficam vazias são removidas.
16
+ Notas sem data resolvível preservam a pasta atual (nunca são perdidas). Idempotente.
17
+
18
+ ## [0.41.0] — 2026-07-16
19
+
20
+ ### Added
21
+
22
+ - Notas derivadas numeradas: bug e aprendizado gerados automaticamente nascem como
23
+ `BUG-NNNN-<slug>.md` / `APR-NNNN-<slug>.md` na pasta do mês (nunca subpasta `DIA N`),
24
+ com frontmatter `bug:`/`apr:` e H1 `# BUG-0001 — <título>` — paridade com o ADR de
25
+ 04-Decisões. Numeração via `getNextDerivedNumber` (scan recursivo, max+1; `getNextAdrNumber`
26
+ virou wrapper).
27
+ - `wendkeep note new --type bug|learning "<título>"`: cria a nota manual já numerada no
28
+ path certo (respeitando locale), com backlink da sessão ativa, e imprime o path — o
29
+ agente nunca calcula número nem pasta à mão. `--date YYYY-MM-DD` opcional.
30
+ - `wendkeep renumber-bugs` e `wendkeep renumber-learnings`: migração retroativa — preview
31
+ por default, `--apply` renomeia em ordem cronológica, MOVE notas de subpastas `DIA N` e
32
+ da raiz para a pasta do mês, normaliza frontmatter/H1, reescreve wikilinks vault-wide
33
+ (full-path e basename) e remove pastas `DIA` vazias. Idempotente.
34
+ - Convenção injetada (VAULT_COMPLEMENT_RULES) e seeds wk-debugging (pt/en) ensinam a
35
+ numeração, a regra sem-DIA e o uso de `wendkeep note new`.
36
+
37
+ ### Fixed
38
+
39
+ - `findLinkedDerivedNotes` (Stop hook) agora varre as pastas derivadas recursivamente —
40
+ antes só enxergava notas na raiz de 04-Decisões/05-Bugs/06-Aprendizados, então notas nas
41
+ subpastas de mês nunca entravam no merge de wikilinks da sessão.
42
+
43
+ ### Migration
44
+
45
+ - Para migrar vaults existentes: `wendkeep renumber-bugs` (revisar preview) →
46
+ `wendkeep renumber-bugs --apply`; idem `renumber-learnings`. Depois
47
+ `wendkeep sync-defs --project . --reseed` para atualizar as skills wk-*.
48
+
7
49
  ## [0.40.0] — 2026-07-16
8
50
 
9
51
  ### Added
package/README.md CHANGED
@@ -138,14 +138,18 @@ Restart Codex and Claude Code after reseeding their generated skills.
138
138
  | `wendkeep cost rebuild [opts]` | Recalcula custos históricos do transcript principal e subagents usando `SESSION_REGISTRY`. Dry-run por padrão; `--apply` atualiza notas e grava `.brain/COST_REBUILD.json`. Aceita `--session`, `--limit` e `--json`. |
139
139
  | `wendkeep session list\|show\|use` | Lista o registry multi-sessão, mostra uma conversa ou muda somente o foco humano de `CURRENT_SESSION.md`. |
140
140
  | `wendkeep change bind <slug> --session <id>` | Vincula ou transfere uma change para uma conversa canônica sem esconder as demais pendências. |
141
-
142
- Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source.
141
+ | `wendkeep note new --type bug\|learning "<title>"` | Create a **numbered** derived note (`BUG-`/`APR-NNNN`) in the month folder and print its vault path. `--date YYYY-MM-DD`. |
142
+ | `wendkeep renumber-decisions` | Renumber `04-Decisões` to `ADR-NNNN-<slug>` chronologically, move notes out of legacy `DIA N` subfolders into the month folder, and rewrite wikilinks. Preview by default; `--apply` / `--json`. |
143
+ | `wendkeep renumber-bugs` | Same for `05-Bugs` → `BUG-NNNN-<slug>`. |
144
+ | `wendkeep renumber-learnings` | Same for `06-Aprendizados`/`06-Learnings` → `APR-NNNN-<slug>`. |
143
145
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
144
146
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
145
147
  | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
146
148
  | `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
147
149
  | `wendkeep --version` / `--help` | Version / usage. |
148
150
 
151
+ Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source.
152
+
149
153
  ## Retroactive memory (`import`) — install today, remember yesterday
150
154
 
151
155
  Install wendkeep into an existing project and it only remembers sessions **from now on**. `wendkeep import` fixes that: one command backfills your project's past **Claude & Codex** sessions into the vault — deduped, dated, with cost — so the graph starts full, not empty. It rebuilds each transcript as a full session note in its **real** date folder — frontmatter (tagged with the transcript's real provider), one iteration block per turn, cost + subagent telemetry, derived decision/bug/learning notes, finalized closing. An offline replay of the live capture flow, so an imported note is indistinguishable from a captured one.
@@ -162,6 +166,43 @@ wendkeep import --vault .myproject-vault --source codex # just Codex
162
166
  - **`--from <dir>`** / **`--codex-from <dir>`** point at the transcript folders explicitly (use if the auto-derived path misses). Also: `--since <date>`, `--limit <n>`, `--json`.
163
167
  - Once imported, `wendkeep cost` aggregates your entire history — retroactively, across both agents.
164
168
 
169
+ ## Derived notes — numbered like ADRs (`note new`, `renumber-*`)
170
+
171
+ Decisions, bugs and learnings are **derived notes**: they live in the month folder of their tree (`<folder>/<year>/<MM-MON>/`) and carry a sequential id — `ADR-0001`, `BUG-0001`, `APR-0001`. One glance tells you what a note is and where it sits in the project's history. No day-level subfolders: a `DIA N` folder holding one note is noise, and it hides the note from folder-wide search.
172
+
173
+ **Creating one** (never write the file by hand — the command owns the number, the folder and the frontmatter):
174
+
175
+ ```bash
176
+ wendkeep note new --type bug "login 500s when the token expires mid-refresh"
177
+ # → 05-Bugs/2026/07-JUL/BUG-0007-login-500s-when-the-token-expires.md
178
+
179
+ wendkeep note new --type learning "a regex without /g only ever returns the first match"
180
+ # → 06-Aprendizados/2026/07-JUL/APR-0003-a-regex-without-g-only-ever-returns.md
181
+ ```
182
+
183
+ It prints the created path, numbers from the current max (recursive scan), files it in the month folder for today (`--date YYYY-MM-DD` to override), and links the active session in `source:` so the graph stays connected. Agents get this rule injected at SessionStart — they call the command instead of guessing a filename.
184
+
185
+ **Migrating an existing vault.** Notes created before `0.41.0` have date-prefixed names (`2026-07-16-bug-<slug>.md`) and may sit in legacy `DIA N` subfolders. One command per tree renumbers them chronologically, moves them up into the month folder, and rewrites every wikilink across the vault:
186
+
187
+ ```bash
188
+ # Bugs — 05-Bugs → BUG-NNNN
189
+ wendkeep renumber-bugs # preview: prints every from → to, writes nothing
190
+ wendkeep renumber-bugs --apply # migrate
191
+
192
+ # Learnings — 06-Aprendizados → APR-NNNN
193
+ wendkeep renumber-learnings # preview
194
+ wendkeep renumber-learnings --apply # migrate
195
+
196
+ # Decisions — 04-Decisões → ADR-NNNN (since 0.30.0)
197
+ wendkeep renumber-decisions # preview
198
+ wendkeep renumber-decisions --apply # migrate
199
+ ```
200
+
201
+ - **Preview is the default.** Nothing is written until `--apply` — read the `from → to` list first; that is where a mangled slug shows up, before it touches your files.
202
+ - **One tree at a time, on purpose.** There is no `renumber-all`: each folder is migrated and reviewed on its own.
203
+ - **Order is chronological**, derived from the note's date (frontmatter → filename prefix → folder), so `BUG-0001` is genuinely the oldest bug — not the first one the scanner happened to read.
204
+ - **Wikilinks are rewritten vault-wide** (full-path and basename forms, aliases preserved), the body's `type`/`bug:`/`apr:`/H1 are normalized, and emptied `DIA` folders are removed. **Idempotent**: a second `--apply` renames nothing. Close Obsidian while migrating, and commit the vault first if it is under git.
205
+
165
206
  ## Change lifecycle — the a2 loop (spec‑driven, native)
166
207
 
167
208
  Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependency loop that keeps *intent* (specs), *work* (changes) and *proof* (sensors) together in the vault, wikilinked into the Obsidian graph.
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
@@ -214,6 +220,21 @@ async function main() {
214
220
  runRenumberDecisions(rest);
215
221
  break;
216
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
+ }
217
238
  case '--version':
218
239
  case '-v':
219
240
  process.stdout.write(`${version()}\n`);
@@ -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
- # Bug - ${title}
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
- # ${L.learn.title} - ${learning.title}
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
- const fileName = issueRef ? `${issueRef}-${causeSlug}.md` : `${dateStr}-bug-${causeSlug}.md`;
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
- const fileName = `${dateStr}-${learningSlug}.md`;
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
- export function getNextAdrNumber(vaultBase) {
665
- const decisionsDir = join(vaultBase, getLocale(vaultBase).folders.decisions);
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(/^ADR-(\d+)/i);
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(decisionsDir);
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);
@@ -5,9 +5,10 @@
5
5
  // chronological order (the order decisions were made), renames the files in place, updates every
6
6
  // wikilink to them across the whole vault, and normalizes each note's `type`/`adr`/H1. Idempotent:
7
7
  // running it again on an already-canonical vault is a no-op (same order, same names).
8
- import { readdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs';
8
+ import { readdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, existsSync } from 'node:fs';
9
9
  import { join, dirname, relative } from 'node:path';
10
10
  import { getLocale } from './locale.mjs';
11
+ import { ensureDir, monthFolderRelFromDateStr } from './obsidian-common.mjs';
11
12
 
12
13
  export const padAdr = (n) => String(n).padStart(4, '0');
13
14
 
@@ -29,17 +30,25 @@ function walkDecisions(vaultBase, decisionsDir) {
29
30
  return out;
30
31
  }
31
32
 
33
+ // Resolve a decision's date: frontmatter `date:` > filename `YYYY-MM-DD` prefix > dated folder
34
+ // (`.../YYYY/MM-.../DIA N/`). Returns '' when nothing is derivable. Used by both the sort key
35
+ // and the destination-folder computation (so a note is filed under the month of its real date).
36
+ export function decisionDate({ abs, base, content }) {
37
+ const c = content ?? (existsSync(abs) ? safeRead(abs) : '');
38
+ const fmDate = c.match(/^date:\s*(\d{4}-\d{2}-\d{2})/m);
39
+ if (fmDate) return fmDate[1];
40
+ const fnDate = base.match(/^(\d{4}-\d{2}-\d{2})/);
41
+ if (fnDate) return fnDate[1];
42
+ const folderDate = String(abs).replaceAll('\\', '/').match(/\/(\d{4})\/(\d{2})-[^/]+\/DIA\s+(\d{1,2})\//i);
43
+ if (folderDate) return `${folderDate[1]}-${folderDate[2]}-${String(folderDate[3]).padStart(2, '0')}`;
44
+ return '';
45
+ }
46
+
32
47
  // Chronological sort key: date, then time, then existing ADR number, then filename — all derived
33
48
  // from (in priority) frontmatter, filename prefix, and the dated folder path.
34
49
  export function decisionSortKey({ abs, base, content }) {
35
50
  const c = content ?? (existsSync(abs) ? safeRead(abs) : '');
36
- const fmDate = c.match(/^date:\s*(\d{4}-\d{2}-\d{2})/m);
37
- const fnDate = base.match(/^(\d{4}-\d{2}-\d{2})/);
38
- const folderDate = abs.replaceAll('\\', '/').match(/\/(\d{4})\/(\d{2})-[^/]+\/DIA\s+(\d{1,2})\//i);
39
- let date = '9999-12-31';
40
- if (fmDate) date = fmDate[1];
41
- else if (fnDate) date = fnDate[1];
42
- else if (folderDate) date = `${folderDate[1]}-${folderDate[2]}-${String(folderDate[3]).padStart(2, '0')}`;
51
+ const date = decisionDate({ abs, base, content: c }) || '9999-12-31';
43
52
 
44
53
  const fmTime = c.match(/^started_at:\s*\S*T(\d{2}:\d{2}:\d{2})/m);
45
54
  const srcTime = c.match(/\[\[[^\]]*\/(\d{2})-(\d{2})-[^\]]*\]\]/); // session wikilink HH-MM prefix
@@ -102,7 +111,7 @@ export function rewriteLinks(content, renames) {
102
111
  return c;
103
112
  }
104
113
 
105
- function allVaultMarkdown(vaultBase) {
114
+ export function allVaultMarkdown(vaultBase) {
106
115
  const out = [];
107
116
  const skip = new Set(['.git', '.obsidian', 'node_modules', '_arquivo']);
108
117
  const walk = (dir) => {
@@ -131,13 +140,16 @@ export function planRenumber(vaultBase) {
131
140
  const num = i + 1;
132
141
  const slug = slugFromDecisionName(n.base);
133
142
  const newBase = `ADR-${padAdr(num)}-${slug}.md`;
134
- const folderRel = dirname(n.rel);
135
- const newRel = folderRel === '.' ? newBase : `${folderRel}/${newBase}`;
143
+ // Destination = the month folder of the note's date (like renumber-derived): flatten legacy
144
+ // `DIA N` subfolders. No derivable date -> keep the current dirname (never lose the note).
145
+ const date = decisionDate(n);
146
+ const destDirRel = date ? monthFolderRelFromDateStr(decisionsDir, date, vaultBase) : dirname(n.rel);
147
+ const newRel = `${destDirRel.replaceAll('\\', '/')}/${newBase}`;
136
148
  const oldAdr = n.base.match(/^ADR-(\d+)/i);
137
149
  renames.push({
138
150
  num, slug,
139
151
  oldAbs: n.abs,
140
- newAbs: join(dirname(n.abs), newBase),
152
+ newAbs: join(vaultBase, destDirRel, newBase),
141
153
  oldRelNoExt: n.rel.replace(/\.md$/i, ''),
142
154
  newRelNoExt: newRel.replace(/\.md$/i, ''),
143
155
  oldBaseNoExt: n.base.replace(/\.md$/i, ''),
@@ -173,11 +185,12 @@ export function renumberDecisions(vaultBase, { apply = false } = {}) {
173
185
  temps.set(r, tmp);
174
186
  });
175
187
 
176
- // Phase B — normalize each note's body, then land it at its final path. For a renamed note we
177
- // write the normalized content back over its temp and renameSync temp -> final (no delete: the
178
- // temp file becomes the final file). For an unchanged name we just rewrite in place.
188
+ // Phase B — normalize each note's body, then land it at its final path (possibly a different
189
+ // month directory). For a renamed note we write the normalized content over its temp and
190
+ // renameSync temp -> final; for an unchanged name we rewrite in place.
179
191
  for (const r of renames) {
180
192
  const tmp = temps.get(r);
193
+ ensureDir(dirname(r.newAbs));
181
194
  if (tmp) {
182
195
  writeFileSync(tmp, normalizeDecisionContent(safeRead(tmp), r.num), 'utf8');
183
196
  renameSync(tmp, r.newAbs);
@@ -194,5 +207,25 @@ export function renumberDecisions(vaultBase, { apply = false } = {}) {
194
207
  const after = rewriteLinks(before, linkRenames);
195
208
  if (after !== before) { writeFileSync(abs, after, 'utf8'); report.filesTouched += 1; report.linksUpdated += 1; }
196
209
  }
210
+
211
+ // Phase D — drop legacy `DIA *` folders left empty by the move to the month folder.
212
+ pruneEmptyDayFolders(vaultBase, getLocale(vaultBase).folders.decisions);
197
213
  return report;
198
214
  }
215
+
216
+ // Remove now-empty `DIA *` folders under the decisions root (best-effort, fail-quiet).
217
+ function pruneEmptyDayFolders(vaultBase, folderRel) {
218
+ const walk = (dir) => {
219
+ let entries;
220
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
221
+ for (const e of entries) {
222
+ if (!e.isDirectory()) continue;
223
+ const abs = join(dir, e.name);
224
+ walk(abs);
225
+ if (/^DIA\s/i.test(e.name)) {
226
+ try { rmdirSync(abs); } catch { /* não-vazio — fica */ }
227
+ }
228
+ }
229
+ };
230
+ walk(join(vaultBase, folderRel));
231
+ }
@@ -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
+ }
@@ -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
- for (const [key, folder] of Object.entries(folders)) {
807
- const dir = join(vaultBase, folder);
808
- for (const fileName of listMarkdownFiles(dir)) {
809
- const absPath = join(dir, fileName);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/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 ADR fix. Renumbers every note in 04-Decisões to
2
- // `ADR-<NNNN>-<slug>` in chronological order, renames the files, and rewrites every wikilink to
3
- // them across the vault. Preview by default; pass --apply to write. Idempotent.
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
- export function runRenumberDecisions(argv) {
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('wendkeep renumber-decisions: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
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 renumber-decisions: vault not found: ${vaultBase}\n`); process.exit(2); }
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 = renumberDecisions(vaultBase, { apply });
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} decisão(ões) · ${report.renamed} a renomear${apply ? ` · ${report.filesTouched} arquivo(s) com links atualizados` : ''}\n`);
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(` ADR-${String(p.num).padStart(4, '0')} ${p.from}\n → ${p.to}\n`);
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
+ }
@@ -65,6 +65,9 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
65
65
  o que você declarou. Sem \`[sensor:]\`, o archive não trava.
66
66
  - A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
67
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.
68
71
  `;
69
72
 
70
73
  const TDD = `# TDD — Red, Green, Refactor (testes que discriminam)
@@ -132,6 +135,13 @@ mudando coisas no escuro.
132
135
  isole uma variável por vez.
133
136
  - Leia a mensagem de erro inteira e a stack — a linha decisiva costuma estar ali.
134
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\`).
135
145
  `;
136
146
 
137
147
  const BRAINSTORMING = `# Brainstorming — ideia vira design
@@ -281,6 +291,9 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
281
291
  \`wendkeep change use <slug>\` or \`--change <slug>\` where available.
282
292
  - No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
283
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.
284
297
  `;
285
298
 
286
299
  const TDD_EN = `# TDD — Red, Green, Refactor (tests that discriminate)
@@ -324,6 +337,13 @@ Use it when something fails or behaves wrong. One hypothesis at a time — no sh
324
337
 
325
338
  Changed several things and it "worked"? You don't know what fixed it — revert, isolate one
326
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).
327
347
  `;
328
348
 
329
349
  const BRAINSTORMING_EN = `# Brainstorming — idea into design
package/src/taxonomy.mjs CHANGED
@@ -46,6 +46,7 @@ export const HOOK_FILES = [
46
46
  'import-sessions.mjs',
47
47
  'decision-capture.mjs',
48
48
  'renumber-decisions.mjs',
49
+ 'renumber-derived.mjs',
49
50
  'subagent-stop.mjs',
50
51
  'task-log.mjs',
51
52
  'vault-health.mjs',