wendkeep 0.41.0 → 0.44.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 +37 -0
- package/README.md +43 -2
- package/hooks/change-core.mjs +4 -6
- package/hooks/linked-notes.mjs +22 -14
- package/hooks/renumber-decisions.mjs +47 -14
- package/hooks/spec-core.mjs +42 -0
- package/package.json +1 -1
- package/src/init.mjs +2 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ 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.44.0] — 2026-07-17
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `wendkeep change new` não cria mais o placeholder `specs/exemplo/spec.md` (nem a pasta
|
|
12
|
+
`specs/`). Era ruído — sempre deletado à mão, e `discoverSpecDeltas` já o filtrava. Quando
|
|
13
|
+
a change resolve `spec_impact: required`, o autor escreve `specs/<capability>/spec.md`
|
|
14
|
+
direto; o formato do delta vive na skill wk-workflow. O filtro de `exemplo` fica (compat
|
|
15
|
+
com changes antigas em voo).
|
|
16
|
+
- README gerado do `07-Specs` reescrito para explicar o ponto mais confundido: specs são
|
|
17
|
+
**por capability, não por mudança** (N changes promovem no mesmo arquivo; o histórico
|
|
18
|
+
por-change vive em `_arquivo`). `promoteSpecs` passa a garantir/atualizar esse README a
|
|
19
|
+
cada archive (`ensureSpecsReadme`), então vaults existentes recebem o texto novo no
|
|
20
|
+
próximo archive — não só os criados via `init`.
|
|
21
|
+
|
|
22
|
+
## [0.43.0] — 2026-07-17
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Dedup de nota derivada era assimétrico: aprendizado deduplicava recursivamente (vault
|
|
27
|
+
inteiro), mas **bug e decisão só olhavam a pasta do mês** (`existingKeysForSession`, scan
|
|
28
|
+
não-recursivo). Uma nota da sessão numa subpasta `DIA` legada não era vista, então um
|
|
29
|
+
re-import/re-captura da mesma sessão criava uma duplicata de bug/decisão. Agora
|
|
30
|
+
`existingKeysForSession` varre a pasta derivada recursivamente (como o de aprendizado),
|
|
31
|
+
mantendo a semântica per-sessão (só notas que referenciam a sessão contam).
|
|
32
|
+
|
|
33
|
+
## [0.42.0] — 2026-07-17
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `wendkeep renumber-decisions --apply` agora **move** as notas de subpastas `DIA N` para a
|
|
38
|
+
pasta do mês da sua data (achatando o legado), consistente com `renumber-bugs`/
|
|
39
|
+
`renumber-learnings` do 0.41.0. Antes só renomeava in-place para `ADR-NNNN-<slug>` e
|
|
40
|
+
deixava as pastas de dia intactas. Os wikilinks já eram reescritos vault-wide; agora
|
|
41
|
+
acompanham o novo caminho de mês, e as pastas `DIA` que ficam vazias são removidas.
|
|
42
|
+
Notas sem data resolvível preservam a pasta atual (nunca são perdidas). Idempotente.
|
|
43
|
+
|
|
7
44
|
## [0.41.0] — 2026-07-16
|
|
8
45
|
|
|
9
46
|
### 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
|
-
|
|
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/hooks/change-core.mjs
CHANGED
|
@@ -110,14 +110,12 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
110
110
|
write('proposta.md', files.proposta);
|
|
111
111
|
write('tarefas.md', files.tarefas);
|
|
112
112
|
if (!existed) write('.spec-impact-v1', '1\n');
|
|
113
|
-
// Auto-sizing (Wave B): a --simple change skips the design
|
|
113
|
+
// Auto-sizing (Wave B): a --simple change skips the design scaffold.
|
|
114
|
+
// No `specs/exemplo` placeholder: it was pure noise (always hand-deleted). When a change
|
|
115
|
+
// resolves `spec_impact: required`, the author writes `specs/<capability>/spec.md` directly
|
|
116
|
+
// — the delta format lives in the wk-workflow skill (and `renderChangeScaffold().specDelta`).
|
|
114
117
|
if (!simple) {
|
|
115
118
|
write('design.md', files.design);
|
|
116
|
-
const exampleDelta = join(dir, 'specs', 'exemplo', 'spec.md');
|
|
117
|
-
if (!existsSync(exampleDelta)) {
|
|
118
|
-
mkdirSync(join(dir, 'specs', 'exemplo'), { recursive: true });
|
|
119
|
-
writeFileSync(exampleDelta, files.specDelta, 'utf8');
|
|
120
|
-
}
|
|
121
119
|
}
|
|
122
120
|
if (!existed) captureSpecBaseline(vaultBase, dir);
|
|
123
121
|
setActiveChange(vaultBase, slug);
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -553,10 +553,6 @@ ${L.learn.futureHint}
|
|
|
553
553
|
|
|
554
554
|
const derivedFoldersFor = (vaultBase) => { const f = getLocale(vaultBase).folders; return { bugs: f.bugs, decisions: f.decisions, learnings: f.learnings }; };
|
|
555
555
|
|
|
556
|
-
function listMd(dir) {
|
|
557
|
-
try { return readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { return []; }
|
|
558
|
-
}
|
|
559
|
-
|
|
560
556
|
// Chaves content_key das derivadas já existentes que linkam esta sessão.
|
|
561
557
|
// Vault-wide learning content_keys (recursive over the learnings folder). existingKeysForSession
|
|
562
558
|
// only looks at the current session + month, so the same lesson re-extracted on a later day/
|
|
@@ -582,19 +578,31 @@ function collectLearningKeys(vaultBase) {
|
|
|
582
578
|
return keys;
|
|
583
579
|
}
|
|
584
580
|
|
|
585
|
-
|
|
581
|
+
// dateStr kept for call compatibility; no longer used to narrow the scan — a session's note may
|
|
582
|
+
// sit in a legacy `DIA` subfolder, not just the month folder, so we walk the whole derived tree
|
|
583
|
+
// (like collectLearningKeys). The per-session semantics stay: only notes referencing THIS session
|
|
584
|
+
// count, so bugs/decisions from other sessions never leak in.
|
|
585
|
+
export function existingKeysForSession(vaultBase, sessionRel, dateStr) { // eslint-disable-line no-unused-vars
|
|
586
586
|
const wikilink = wikilinkFromRel(sessionRel);
|
|
587
587
|
const out = { bugs: [], decisions: [], learnings: [] };
|
|
588
588
|
for (const [type, folder] of Object.entries(derivedFoldersFor(vaultBase))) {
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
589
|
+
const root = join(vaultBase, folder);
|
|
590
|
+
const walk = (d) => {
|
|
591
|
+
let entries;
|
|
592
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
593
|
+
for (const e of entries) {
|
|
594
|
+
const p = join(d, e.name);
|
|
595
|
+
if (e.isDirectory()) { walk(p); continue; }
|
|
596
|
+
if (!e.name.endsWith('.md')) continue;
|
|
597
|
+
try {
|
|
598
|
+
const c = readFileSync(p, 'utf-8');
|
|
599
|
+
if (!c.includes(sessionRel) && !c.includes(wikilink)) continue;
|
|
600
|
+
const m = c.match(/^content_key:\s*"?(.*?)"?\s*$/m);
|
|
601
|
+
if (m && m[1]) out[type].push(m[1]);
|
|
602
|
+
} catch { /* ignora nota ilegível */ }
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
walk(root);
|
|
598
606
|
}
|
|
599
607
|
return out;
|
|
600
608
|
}
|
|
@@ -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
|
|
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
|
|
@@ -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
|
-
|
|
135
|
-
|
|
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(
|
|
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
|
|
177
|
-
// write the normalized content
|
|
178
|
-
// temp
|
|
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
|
+
}
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -317,6 +317,47 @@ export function discoverSpecDeltas(changeDir) {
|
|
|
317
317
|
return caps;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// The 07-Specs README, explaining the most-confused point: specs are per-CAPABILITY, not
|
|
321
|
+
// per-change (many changes promote into the same file; the per-change record lives in
|
|
322
|
+
// _arquivo). Generated + read-only. Written on init and refreshed on every archive so
|
|
323
|
+
// existing vaults self-heal. Bilingual by vault locale.
|
|
324
|
+
export function ensureSpecsReadme(vaultBase) {
|
|
325
|
+
const loc = getLocale(vaultBase);
|
|
326
|
+
const en = loc.id === 'en';
|
|
327
|
+
const dir = join(vaultBase, loc.folders.specs);
|
|
328
|
+
ensureDir(dir);
|
|
329
|
+
const body = en
|
|
330
|
+
? `# Specs — generated living contract
|
|
331
|
+
|
|
332
|
+
**One file per _capability_, not per change.** Each file is the current, cumulative contract
|
|
333
|
+
of a capability — the sum of every change's spec delta that was promoted here. Many changes
|
|
334
|
+
touch the same capability, so N changes collapse into far fewer spec files.
|
|
335
|
+
|
|
336
|
+
Think of it like source code vs commits: this folder is the *current code* of each capability;
|
|
337
|
+
the *commits* that built it are the archived changes in \`${loc.folders.changes}/_arquivo/\`.
|
|
338
|
+
|
|
339
|
+
- **Generated + read-only.** Never author here. Write deltas only in
|
|
340
|
+
\`${loc.folders.changes}/<slug>/specs/<capability>/spec.md\` (ADDED/MODIFIED/REMOVED);
|
|
341
|
+
\`wendkeep change archive\` promotes them into this folder.
|
|
342
|
+
- Per-change history → \`${loc.folders.changes}/_arquivo/\`. Current contract → here.
|
|
343
|
+
`
|
|
344
|
+
: `# Specs — contrato consolidado gerado
|
|
345
|
+
|
|
346
|
+
**Um arquivo por _capability_, não por mudança.** Cada arquivo é o contrato atual e acumulado
|
|
347
|
+
de uma capability — a soma de todos os deltas de spec promovidos aqui. Várias mudanças tocam a
|
|
348
|
+
mesma capability, então N mudanças colapsam em bem menos specs.
|
|
349
|
+
|
|
350
|
+
Pense como código-fonte vs commits: esta pasta é o *código atual* de cada capability; os
|
|
351
|
+
*commits* que a construíram são as mudanças arquivadas em \`${loc.folders.changes}/_arquivo/\`.
|
|
352
|
+
|
|
353
|
+
- **Gerado + somente leitura.** Nunca edite aqui. Escreva deltas apenas em
|
|
354
|
+
\`${loc.folders.changes}/<slug>/specs/<capability>/spec.md\` (ADDED/MODIFIED/REMOVED); o
|
|
355
|
+
\`wendkeep change archive\` promove para esta pasta.
|
|
356
|
+
- Histórico por mudança → \`${loc.folders.changes}/_arquivo/\`. Contrato atual → aqui.
|
|
357
|
+
`;
|
|
358
|
+
writeFileSync(join(dir, 'README.md'), body, 'utf8');
|
|
359
|
+
}
|
|
360
|
+
|
|
320
361
|
// Merge each capability's delta (in the change) into the living spec in 07-Specs.
|
|
321
362
|
export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, dateStr } = {}) {
|
|
322
363
|
const loc = getLocale(vaultBase);
|
|
@@ -347,6 +388,7 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
347
388
|
promoted.push(cap);
|
|
348
389
|
}
|
|
349
390
|
recordPromotedSpecs(vaultBase, promoted);
|
|
391
|
+
ensureSpecsReadme(vaultBase); // self-heal the explainer so existing vaults get it on archive
|
|
350
392
|
return { promoted, warnings };
|
|
351
393
|
}
|
|
352
394
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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/init.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { seedDefinitions, syncDefs } from './sync-defs.mjs';
|
|
|
40
40
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
41
41
|
import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
|
|
42
42
|
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
|
|
43
|
-
import { adoptSpecsState, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
|
|
43
|
+
import { adoptSpecsState, ensureSpecsReadme, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
|
|
44
44
|
import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
|
|
45
45
|
|
|
46
46
|
function parseArgs(argv) {
|
|
@@ -490,12 +490,7 @@ export async function runInit(argv) {
|
|
|
490
490
|
seedWkSkills(brainDir, loc.id); // Pilar A: native process skills, in the vault locale.
|
|
491
491
|
// Seed the change/spec layer starters (Pilar B) — non-destructive.
|
|
492
492
|
const en = loc.id === 'en';
|
|
493
|
-
|
|
494
|
-
if (!existsSync(specsReadme)) {
|
|
495
|
-
writeFileSync(specsReadme, en
|
|
496
|
-
? `# Specs — generated living contract\n\nRead-only: do not author here. Write deltas only in \`${loc.folders.changes}/<slug>/specs/\`; archive promotes them here.\n`
|
|
497
|
-
: `# Specs — contrato consolidado gerado\n\nSomente leitura: não edite aqui. Escreva deltas apenas em \`${loc.folders.changes}/<slug>/specs/\`; o archive promove para cá.\n`, 'utf8');
|
|
498
|
-
}
|
|
493
|
+
ensureSpecsReadme(vaultPath); // same explainer promoteSpecs refreshes (per-capability, read-only)
|
|
499
494
|
if (!existsSync(join(vaultPath, SPECS_STATE_FILE))) {
|
|
500
495
|
const livingSpecs = readdirSync(join(vaultPath, loc.folders.specs)).filter((name) => name.endsWith('.md') && name !== 'README.md');
|
|
501
496
|
if (!livingSpecs.length) adoptSpecsState(vaultPath);
|