wendkeep 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +36 -3
- package/bin/wendkeep.mjs +14 -1
- package/hooks/brain-core.mjs +2 -1
- package/hooks/change-core.mjs +45 -34
- package/hooks/harness-doctor.mjs +4 -3
- package/hooks/linked-notes.mjs +8 -6
- package/hooks/locale.mjs +73 -0
- package/hooks/obsidian-common.mjs +12 -9
- package/hooks/session-ensure.mjs +1 -1
- package/hooks/session-start.mjs +1 -1
- package/hooks/session-stop.mjs +5 -3
- package/hooks/spec-core.mjs +10 -7
- package/hooks/vault-health.mjs +3 -1
- package/package.json +2 -1
- package/schema/wendkeep.sensors.schema.json +34 -0
- package/src/change.mjs +84 -4
- package/src/dotcontext-seed.mjs +1 -1
- package/src/init.mjs +24 -8
- package/src/sensors.mjs +23 -0
- package/src/spec.mjs +54 -0
- package/src/sync-defs.mjs +61 -2
- package/src/taxonomy.mjs +1 -0
- package/src/validate-core.mjs +34 -9
- package/src/vault-theme.mjs +40 -30
- package/src/verify.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ 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.8.0] — 2026-07-05
|
|
8
|
+
|
|
9
|
+
Reach: internationalization + agent-agnostic distribution.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Vault locale (i18n):** `wendkeep init --locale en` creates an English vault
|
|
13
|
+
(`02-Sessions`, `04-Decisions`, `08-Changes`, …, English months, English change scaffold,
|
|
14
|
+
English CORE skeleton, localized theme/graph groups). The locale is a vault property
|
|
15
|
+
(`.brain/config.json`), locked at init; absent = `pt-BR` — existing vaults are untouched
|
|
16
|
+
and never renamed. Parsers are **bilingual everywhere** (`Requisito|Requirement`,
|
|
17
|
+
`mata mutante|kill mutant`, CORE section sets), so mixed content never breaks.
|
|
18
|
+
- **AGENTS.md managed section:** `sync-defs`/`init` maintain a marker-delimited section in
|
|
19
|
+
the project's `AGENTS.md` (loop summary + skill inventory) — one file that Codex, Amp,
|
|
20
|
+
Cursor, Zed and any AGENTS.md-reading agent picks up. User content around it is preserved.
|
|
21
|
+
- **Harness contract v1.2** (`docs/14`): locale + AGENTS.md channel.
|
|
22
|
+
|
|
23
|
+
### Deferred
|
|
24
|
+
- Extra mutation-report formats (mutmut/PIT) and per-agent session-hook wiring — backlog
|
|
25
|
+
(`docs/17`).
|
|
26
|
+
|
|
27
|
+
## [0.7.0] — 2026-07-05
|
|
28
|
+
|
|
29
|
+
Ergonomics: the loop without hand-editing files.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **`change status`** — one screen: tasks (done/open with `[req:]`/`[sensor:]`), sensor
|
|
33
|
+
evidence, verdict state (ok / stale / incomplete / absent), mutation round.
|
|
34
|
+
- **`change done <id>` / `undone <id>`** — tick tasks from the CLI (exact-id anchored).
|
|
35
|
+
- **`change diff`** — dry-run preview of the spec promotion (`+` ADDED / `~` MODIFIED /
|
|
36
|
+
`-` REMOVED / `!` warnings) without touching `07-Specs`.
|
|
37
|
+
- **`spec list` / `spec show <capability>`** — read-only views over the living specs.
|
|
38
|
+
- **`sensors list`** — the sensors from `wendkeep.sensors.json`; a **JSON Schema** for the
|
|
39
|
+
file now ships in the package (`schema/`) and the init seed points `$schema` at it.
|
|
40
|
+
- README: "the loop in five minutes" worked example.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- `change` subcommands without a positional argument no longer mistake the `--vault` value
|
|
44
|
+
for a slug.
|
|
45
|
+
|
|
7
46
|
## [0.6.1] — 2026-07-05
|
|
8
47
|
|
|
9
48
|
Hardening: CI + real-world gate holes found by self-audit.
|
|
@@ -141,6 +180,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
141
180
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
142
181
|
|
|
143
182
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
183
|
+
[0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
|
|
184
|
+
[0.7.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.7.0
|
|
144
185
|
[0.6.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.1
|
|
145
186
|
[0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
|
|
146
187
|
[0.5.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.5.0
|
package/README.md
CHANGED
|
@@ -70,8 +70,11 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
70
70
|
|---|---|
|
|
71
71
|
| `wendkeep init` | Set up wendkeep in a project (vault taxonomy + settings + MCP + skills). |
|
|
72
72
|
| `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
|
|
73
|
-
| `wendkeep change <sub>` | Change lifecycle: `new
|
|
74
|
-
| `wendkeep verify [--
|
|
73
|
+
| `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` / `show` / `status` / `done <id>` / `undone <id>` / `diff` / `archive [--force]`. |
|
|
74
|
+
| `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
|
|
75
|
+
| `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
|
|
76
|
+
| `wendkeep sensors list` | Show the sensors from `wendkeep.sensors.json` (JSON Schema included in the package). |
|
|
77
|
+
| `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
75
78
|
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
|
|
76
79
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
|
77
80
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
@@ -90,7 +93,37 @@ explore → propose → apply (TDD) → verify → archive
|
|
|
90
93
|
- **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
|
|
91
94
|
- **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
|
|
92
95
|
|
|
93
|
-
`wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
|
|
96
|
+
`wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
|
|
97
|
+
|
|
98
|
+
### The loop in five minutes
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx wendkeep init --yes # vault + hooks + sensors + skills
|
|
102
|
+
npx wendkeep change new dark-mode # proposta/design/tarefas — change is now active
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Edit `tarefas.md` — tag proof and requirement per task:
|
|
106
|
+
|
|
107
|
+
```markdown
|
|
108
|
+
- [ ] 1.1 toggle persists across sessions [req:UI-1] [sensor:tests]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
|
|
112
|
+
`specs/ui/spec.md` (`## ADDED Requirements` → `### Requisito: UI-1 — dark mode toggle`). Then:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx wendkeep change status # one screen: tasks / sensors / verdict
|
|
116
|
+
npx wendkeep change done 1.1 # tick a task from the CLI
|
|
117
|
+
npx wendkeep verify # run the declared sensors -> evidencia.json
|
|
118
|
+
npx wendkeep verify --deep # assemble the verification package
|
|
119
|
+
# the wk-verify skill (fresh, read-only pass) writes verdict.json
|
|
120
|
+
npx wendkeep change diff # preview what will land in 07-Specs
|
|
121
|
+
npx wendkeep change archive dark-mode # gate: sensors + verdict + no open tasks
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The archive promotes the delta into the living `07-Specs/ui.md`, mints an ADR, and the
|
|
125
|
+
Obsidian graph now links *session ↔ change ↔ requirement ↔ decision*. A change that names
|
|
126
|
+
no `[req:]` skips the independent verdict — the sensor gate is its proof.
|
|
94
127
|
|
|
95
128
|
## How it works
|
|
96
129
|
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -43,7 +43,10 @@ Usage:
|
|
|
43
43
|
agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
|
|
44
44
|
|
|
45
45
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
46
|
-
wendkeep change <sub> Change lifecycle: new
|
|
46
|
+
wendkeep change <sub> Change lifecycle: new [--simple] | list | show | status |
|
|
47
|
+
done <id> | undone <id> | diff | archive [--force].
|
|
48
|
+
wendkeep spec <sub> Living specs: list | show <capability>.
|
|
49
|
+
wendkeep sensors list Show the sensors from wendkeep.sensors.json.
|
|
47
50
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
48
51
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
49
52
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
@@ -117,6 +120,16 @@ async function main() {
|
|
|
117
120
|
runLesson(rest);
|
|
118
121
|
break;
|
|
119
122
|
}
|
|
123
|
+
case 'spec': {
|
|
124
|
+
const { runSpec } = await import('../src/spec.mjs');
|
|
125
|
+
runSpec(rest);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'sensors': {
|
|
129
|
+
const { runSensors } = await import('../src/sensors.mjs');
|
|
130
|
+
runSensors(rest);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
120
133
|
case '--version':
|
|
121
134
|
case '-v':
|
|
122
135
|
process.stdout.write(`${version()}\n`);
|
package/hooks/brain-core.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
|
|
6
|
+
import { getLocale } from './locale.mjs';
|
|
6
7
|
|
|
7
8
|
export function brainDir(vaultBase) {
|
|
8
9
|
return join(vaultBase, '.brain');
|
|
@@ -60,7 +61,7 @@ function derivedLinks(content) {
|
|
|
60
61
|
// Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
|
|
61
62
|
export function buildBrainIndex(vaultBase) {
|
|
62
63
|
const rows = [];
|
|
63
|
-
for (const fp of walkMd(join(vaultBase,
|
|
64
|
+
for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
|
|
64
65
|
let content;
|
|
65
66
|
try { content = readFileSync(fp, 'utf8'); } catch { continue; }
|
|
66
67
|
const fm = parseFrontmatter(content);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
// Native change/spec lifecycle in the vault (Pilar B). Vault-facing lib consumed by
|
|
3
3
|
// the `wendkeep change` CLI (src/change.mjs) and the brain-inject hook. No external deps.
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
|
|
7
7
|
import { parseSpecsList, promoteSpecs } from './spec-core.mjs';
|
|
8
|
+
import { getLocale } from './locale.mjs';
|
|
8
9
|
|
|
9
|
-
export const CHANGES_DIR = '08-Mudanças';
|
|
10
|
-
export const SPECS_DIR = '07-Specs';
|
|
11
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
12
11
|
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
13
12
|
|
|
14
|
-
export function changeDirRel(slug) {
|
|
15
|
-
return join(
|
|
13
|
+
export function changeDirRel(slug, vaultBase) {
|
|
14
|
+
return join(getLocale(vaultBase).folders.changes, slug);
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export function renderChangeScaffold({ slug, sessionRel, dateStr }) {
|
|
17
|
+
export function renderChangeScaffold({ slug, sessionRel, dateStr, locale = 'pt-BR' }) {
|
|
18
|
+
const en = locale === 'en';
|
|
19
19
|
const source = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
|
|
20
20
|
const proposta = `---
|
|
21
21
|
type: change
|
|
@@ -31,27 +31,20 @@ specs: []
|
|
|
31
31
|
|
|
32
32
|
# ${slug}
|
|
33
33
|
|
|
34
|
-
## Por
|
|
35
|
-
|
|
36
|
-
(motivo da mudança)
|
|
37
|
-
|
|
38
|
-
## O que muda
|
|
39
|
-
|
|
40
|
-
(escopo da mudança)
|
|
34
|
+
${en ? '## Why\n\n(reason for the change)\n\n## What changes\n\n(scope of the change)' : '## Por quê\n\n(motivo da mudança)\n\n## O que muda\n\n(escopo da mudança)'}
|
|
41
35
|
`;
|
|
42
36
|
const design = `# ${slug} — design
|
|
43
37
|
|
|
44
|
-
## Abordagem
|
|
45
|
-
|
|
46
|
-
(abordagem técnica)
|
|
38
|
+
${en ? '## Approach\n\n(technical approach)' : '## Abordagem\n\n(abordagem técnica)'}
|
|
47
39
|
`;
|
|
48
|
-
const tarefas = `# ${slug} — tarefas
|
|
40
|
+
const tarefas = `# ${slug} — ${en ? 'tasks' : 'tarefas'}
|
|
49
41
|
|
|
50
|
-
- [ ] 1.1 (primeira tarefa)
|
|
42
|
+
- [ ] 1.1 ${en ? '(first task)' : '(primeira tarefa)'}
|
|
51
43
|
`;
|
|
44
|
+
const reqHeading = en ? 'Requirement' : 'Requisito';
|
|
52
45
|
const specDelta = `## ADDED Requirements
|
|
53
|
-
###
|
|
54
|
-
(comportamento / cenários)
|
|
46
|
+
### ${reqHeading}: ${en ? '(name)' : '(nome)'}
|
|
47
|
+
${en ? '(behaviour / scenarios)' : '(comportamento / cenários)'}
|
|
55
48
|
|
|
56
49
|
## MODIFIED Requirements
|
|
57
50
|
|
|
@@ -80,10 +73,11 @@ export function clearActiveChange(vaultBase) {
|
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple = false }) {
|
|
83
|
-
const
|
|
76
|
+
const loc = getLocale(vaultBase);
|
|
77
|
+
const dir = join(vaultBase, loc.folders.changes, slug);
|
|
84
78
|
const existed = existsSync(join(dir, 'proposta.md'));
|
|
85
79
|
mkdirSync(dir, { recursive: true });
|
|
86
|
-
const files = renderChangeScaffold({ slug, sessionRel, dateStr });
|
|
80
|
+
const files = renderChangeScaffold({ slug, sessionRel, dateStr, locale: loc.id });
|
|
87
81
|
const write = (name, content) => {
|
|
88
82
|
const f = join(dir, name);
|
|
89
83
|
if (!existsSync(f)) writeFileSync(f, content, 'utf8');
|
|
@@ -100,7 +94,7 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
100
94
|
}
|
|
101
95
|
}
|
|
102
96
|
setActiveChange(vaultBase, slug);
|
|
103
|
-
return { rel: changeDirRel(slug), created: !existed };
|
|
97
|
+
return { rel: changeDirRel(slug, vaultBase), created: !existed };
|
|
104
98
|
}
|
|
105
99
|
|
|
106
100
|
export function parseTasks(md) {
|
|
@@ -123,8 +117,19 @@ export function parseTasks(md) {
|
|
|
123
117
|
return tasks;
|
|
124
118
|
}
|
|
125
119
|
|
|
120
|
+
// Toggle a task checkbox by its exact id (0.7.0 ergonomics). Returns false when absent.
|
|
121
|
+
export function setTaskDone(changeDir, taskId, done = true) {
|
|
122
|
+
const path = join(changeDir, 'tarefas.md');
|
|
123
|
+
const md = readFileSync(path, 'utf8');
|
|
124
|
+
const esc = String(taskId).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
125
|
+
const re = new RegExp(`^(-\\s+\\[)( |x)(\\]\\s+${esc}\\s)`, 'm');
|
|
126
|
+
if (!re.test(md)) return false;
|
|
127
|
+
writeFileSync(path, md.replace(re, `$1${done ? 'x' : ' '}$3`), 'utf8');
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
export function listChanges(vaultBase) {
|
|
127
|
-
const base = join(vaultBase,
|
|
132
|
+
const base = join(vaultBase, getLocale(vaultBase).folders.changes);
|
|
128
133
|
const active = [];
|
|
129
134
|
let archived = [];
|
|
130
135
|
try {
|
|
@@ -142,29 +147,33 @@ export function listChanges(vaultBase) {
|
|
|
142
147
|
export function buildActiveChangeInjection(vaultBase, { maxTasks = 8 } = {}) {
|
|
143
148
|
const slug = activeChange(vaultBase);
|
|
144
149
|
if (!slug) return '';
|
|
150
|
+
const chDir = getLocale(vaultBase).folders.changes;
|
|
145
151
|
let md = '';
|
|
146
|
-
try { md = readFileSync(join(vaultBase,
|
|
152
|
+
try { md = readFileSync(join(vaultBase, chDir, slug, 'tarefas.md'), 'utf8'); } catch { return ''; }
|
|
147
153
|
const open = parseTasks(md).filter((t) => !t.done).slice(0, maxTasks);
|
|
148
154
|
const lines = open.map((t) => `- [ ] ${t.id} ${t.text}`);
|
|
149
155
|
const more = open.length === maxTasks ? '\n*…mais tarefas em tarefas.md*' : '';
|
|
150
156
|
return `<active_change>
|
|
151
|
-
Mudança ativa: ${slug} — [[${
|
|
157
|
+
Mudança ativa: ${slug} — [[${chDir}/${slug}/proposta]]. Tarefas abertas:
|
|
152
158
|
${lines.join('\n')}${more}
|
|
153
159
|
</active_change>`;
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
export function activeChangeLink(vaultBase) {
|
|
157
163
|
const slug = activeChange(vaultBase);
|
|
158
|
-
return slug ? `Change ativa: [[${
|
|
164
|
+
return slug ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${slug}/proposta]]` : '';
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
// Append fix tasks for surviving mutants to a change's tarefas.md (Wave B). Deduped by
|
|
162
168
|
// file:line, numbered M.<n> continuing from any existing fix tasks. Returns count added.
|
|
163
169
|
export function appendFixTasks(changeDir, mutants, sensorId) {
|
|
164
170
|
const path = join(changeDir, 'tarefas.md');
|
|
171
|
+
// changeDir = <vault>/<changesDir>/<slug> — derive the vault for the locale verb.
|
|
172
|
+
const verb = getLocale(dirname(dirname(changeDir))).fixTaskVerb;
|
|
165
173
|
let md = '';
|
|
166
174
|
try { md = readFileSync(path, 'utf8'); } catch { /* nova */ }
|
|
167
|
-
|
|
175
|
+
// Dedup is bilingual so a vault that switched locale mid-change never duplicates.
|
|
176
|
+
const existing = new Set([...md.matchAll(/(?:mata mutante|kill mutant) (\S+):(\d+)/g)].map((m) => `${m[1]}:${m[2]}`));
|
|
168
177
|
const nums = [...md.matchAll(/^-\s+\[[ x]\]\s+M\.(\d+)\b/gm)].map((m) => Number(m[1]));
|
|
169
178
|
let n = nums.length ? Math.max(...nums) : 0;
|
|
170
179
|
const lines = [];
|
|
@@ -173,7 +182,7 @@ export function appendFixTasks(changeDir, mutants, sensorId) {
|
|
|
173
182
|
if (existing.has(key)) continue;
|
|
174
183
|
existing.add(key);
|
|
175
184
|
n += 1;
|
|
176
|
-
lines.push(`- [ ] M.${n}
|
|
185
|
+
lines.push(`- [ ] M.${n} ${verb} ${mut.file}:${mut.line} (${mut.mutator}) [sensor:${sensorId}]`);
|
|
177
186
|
}
|
|
178
187
|
if (!lines.length) return 0;
|
|
179
188
|
const sep = md === '' || md.endsWith('\n') ? '' : '\n';
|
|
@@ -187,11 +196,13 @@ export function gateGreen() {
|
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrNum }) {
|
|
190
|
-
const
|
|
199
|
+
const loc = getLocale(vaultBase);
|
|
200
|
+
const chDir = loc.folders.changes;
|
|
201
|
+
const src = join(vaultBase, chDir, slug);
|
|
191
202
|
const verdict = gate(src);
|
|
192
203
|
if (!verdict.ok) return { ok: false, failing: verdict.failing || [] };
|
|
193
204
|
|
|
194
|
-
const destRel = join(
|
|
205
|
+
const destRel = join(chDir, ARCHIVE_DIR, `${dateStr}-${slug}`);
|
|
195
206
|
const changeWikilink = wikilinkFromRel(join(destRel, 'proposta'));
|
|
196
207
|
|
|
197
208
|
// Promote spec deltas into the living 07-Specs BEFORE moving (deltas live in src).
|
|
@@ -209,16 +220,16 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
209
220
|
let reqIds = [];
|
|
210
221
|
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
|
|
211
222
|
|
|
212
|
-
ensureDir(join(vaultBase,
|
|
223
|
+
ensureDir(join(vaultBase, chDir, ARCHIVE_DIR));
|
|
213
224
|
renameSync(src, join(vaultBase, destRel));
|
|
214
225
|
|
|
215
226
|
const [year] = String(dateStr).split('-');
|
|
216
|
-
const adrDirRel = join(
|
|
227
|
+
const adrDirRel = join(loc.folders.decisions, year);
|
|
217
228
|
ensureDir(join(vaultBase, adrDirRel));
|
|
218
229
|
const num = String(adrNum).padStart(3, '0');
|
|
219
230
|
const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
|
|
220
231
|
const capLine = promoted.length
|
|
221
|
-
? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(
|
|
232
|
+
? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(loc.folders.specs, c))).join(', ')}.`
|
|
222
233
|
: '';
|
|
223
234
|
const reqLine = reqIds.length ? `\n\nRequisitos: ${reqIds.join(', ')}.` : '';
|
|
224
235
|
writeFileSync(join(vaultBase, adrRel), `---
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -4,11 +4,12 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { activeChange, parseTasks } from './change-core.mjs';
|
|
6
6
|
import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict } from './spec-core.mjs';
|
|
7
|
-
|
|
8
|
-
const CHANGES_DIR = '08-Mudanças';
|
|
9
|
-
const SPECS_DIR = '07-Specs';
|
|
7
|
+
import { getLocale } from './locale.mjs';
|
|
10
8
|
|
|
11
9
|
export function checkHarness(vaultBase, projectRoot) {
|
|
10
|
+
const loc = getLocale(vaultBase);
|
|
11
|
+
const CHANGES_DIR = loc.folders.changes;
|
|
12
|
+
const SPECS_DIR = loc.folders.specs;
|
|
12
13
|
const errors = [];
|
|
13
14
|
const warnings = [];
|
|
14
15
|
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
toVaultRelative,
|
|
13
13
|
wikilinkFromRel,
|
|
14
14
|
} from './obsidian-common.mjs';
|
|
15
|
+
import { getLocale } from './locale.mjs';
|
|
15
16
|
|
|
16
17
|
function yamlQuote(value) {
|
|
17
18
|
return `"${String(value || '').replaceAll('"', '\\"')}"`;
|
|
@@ -446,7 +447,7 @@ _Registrar como este conhecimento pode ser reutilizado._
|
|
|
446
447
|
`;
|
|
447
448
|
}
|
|
448
449
|
|
|
449
|
-
const
|
|
450
|
+
const derivedFoldersFor = (vaultBase) => { const f = getLocale(vaultBase).folders; return { bugs: f.bugs, decisions: f.decisions, learnings: f.learnings }; };
|
|
450
451
|
|
|
451
452
|
function listMd(dir) {
|
|
452
453
|
try { return readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { return []; }
|
|
@@ -456,8 +457,8 @@ function listMd(dir) {
|
|
|
456
457
|
function existingKeysForSession(vaultBase, sessionRel, dateStr) {
|
|
457
458
|
const wikilink = wikilinkFromRel(sessionRel);
|
|
458
459
|
const out = { bugs: [], decisions: [], learnings: [] };
|
|
459
|
-
for (const [type, folder] of Object.entries(
|
|
460
|
-
const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr));
|
|
460
|
+
for (const [type, folder] of Object.entries(derivedFoldersFor(vaultBase))) {
|
|
461
|
+
const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr, vaultBase));
|
|
461
462
|
for (const fileName of listMd(dir)) {
|
|
462
463
|
try {
|
|
463
464
|
const c = readFileSync(join(dir, fileName), 'utf-8');
|
|
@@ -477,9 +478,10 @@ function alreadyHasKey(keys, candidate) {
|
|
|
477
478
|
export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options = {}) {
|
|
478
479
|
const linked = { decisions: [], bugs: [], learnings: [] };
|
|
479
480
|
const provider = providerMeta(options.provider);
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
const
|
|
481
|
+
const locF = getLocale(vaultBase).folders;
|
|
482
|
+
const bugsDir = join(vaultBase, monthFolderRelFromDateStr(locF.bugs, dateStr, vaultBase));
|
|
483
|
+
const decisionsDir = join(vaultBase, monthFolderRelFromDateStr(locF.decisions, dateStr, vaultBase));
|
|
484
|
+
const learningsDir = join(vaultBase, monthFolderRelFromDateStr(locF.learnings, dateStr, vaultBase));
|
|
483
485
|
ensureDir(bugsDir);
|
|
484
486
|
ensureDir(decisionsDir);
|
|
485
487
|
ensureDir(learningsDir);
|
package/hooks/locale.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// hooks/locale.mjs — vault locale (i18n, 0.8.0). The locale is a property of the VAULT,
|
|
2
|
+
// stored at <vault>/.brain/config.json ({ "locale": "en" }); absent = pt-BR (full backward
|
|
3
|
+
// compat). Parsers stay bilingual everywhere; only RENDERING follows the locale.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const LOCALES = {
|
|
8
|
+
'pt-BR': {
|
|
9
|
+
id: 'pt-BR',
|
|
10
|
+
folders: {
|
|
11
|
+
inbox: '00-Inbox',
|
|
12
|
+
project: '01-Projeto',
|
|
13
|
+
sessions: '02-Sessões',
|
|
14
|
+
linear: '03-Linear',
|
|
15
|
+
decisions: '04-Decisões',
|
|
16
|
+
bugs: '05-Bugs',
|
|
17
|
+
learnings: '06-Aprendizados',
|
|
18
|
+
specs: '07-Specs',
|
|
19
|
+
changes: '08-Mudanças',
|
|
20
|
+
},
|
|
21
|
+
months: ['01-JAN', '02-FEV', '03-MAR', '04-ABR', '05-MAI', '06-JUN', '07-JUL', '08-AGO', '09-SET', '10-OUT', '11-NOV', '12-DEZ'],
|
|
22
|
+
reqHeading: 'Requisito',
|
|
23
|
+
fixTaskVerb: 'mata mutante',
|
|
24
|
+
coreSections: ['Preferências do Usuário', 'Padrões Ativos', 'Pendências Abertas'],
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
id: 'en',
|
|
28
|
+
folders: {
|
|
29
|
+
inbox: '00-Inbox',
|
|
30
|
+
project: '01-Project',
|
|
31
|
+
sessions: '02-Sessions',
|
|
32
|
+
linear: '03-Linear',
|
|
33
|
+
decisions: '04-Decisions',
|
|
34
|
+
bugs: '05-Bugs',
|
|
35
|
+
learnings: '06-Learnings',
|
|
36
|
+
specs: '07-Specs',
|
|
37
|
+
changes: '08-Changes',
|
|
38
|
+
},
|
|
39
|
+
months: ['01-JAN', '02-FEB', '03-MAR', '04-APR', '05-MAY', '06-JUN', '07-JUL', '08-AUG', '09-SEP', '10-OCT', '11-NOV', '12-DEC'],
|
|
40
|
+
reqHeading: 'Requirement',
|
|
41
|
+
fixTaskVerb: 'kill mutant',
|
|
42
|
+
coreSections: ['User Preferences', 'Active Patterns', 'Open Items'],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_LOCALE = 'pt-BR';
|
|
47
|
+
|
|
48
|
+
// Per-process cache: one vault per process (hooks + CLI), reads are hot paths.
|
|
49
|
+
const cache = new Map();
|
|
50
|
+
|
|
51
|
+
export function getLocale(vaultBase) {
|
|
52
|
+
if (!vaultBase) return LOCALES[DEFAULT_LOCALE];
|
|
53
|
+
const key = String(vaultBase);
|
|
54
|
+
if (cache.has(key)) return cache.get(key);
|
|
55
|
+
let id = DEFAULT_LOCALE;
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(readFileSync(join(key, '.brain', 'config.json'), 'utf8'));
|
|
58
|
+
if (data.locale && LOCALES[data.locale]) id = data.locale;
|
|
59
|
+
} catch { /* sem config = pt-BR */ }
|
|
60
|
+
const loc = LOCALES[id];
|
|
61
|
+
cache.set(key, loc);
|
|
62
|
+
return loc;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Test hook: drop the memoized entry (tests rewrite config.json under one tmpdir).
|
|
66
|
+
export function clearLocaleCache() {
|
|
67
|
+
cache.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The full vault taxonomy for a locale (folders + fixed entries), in creation order.
|
|
71
|
+
export function vaultFolders(loc) {
|
|
72
|
+
return [...Object.values(loc.folders), 'Templates', '.brain'];
|
|
73
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, dirname, join, relative } from 'path';
|
|
4
|
+
import { getLocale } from './locale.mjs';
|
|
4
5
|
|
|
5
6
|
// Neutral fallback only. The vault is normally resolved from the
|
|
6
7
|
// OBSIDIAN_VAULT_PATH env var (set by `wendkeep init`) via getVaultBase() below.
|
|
@@ -128,26 +129,28 @@ export function formatLocalIso(date = new Date()) {
|
|
|
128
129
|
return `${formatDate(date)}T${formatTime(date)}`;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
// Locale (0.8.0): month labels + folder names come from the vault locale when a
|
|
133
|
+
// vaultBase is given; without it, pt-BR (backward compat — every legacy caller).
|
|
134
|
+
export function datedFolderRel(rootFolder, date = new Date(), vaultBase) {
|
|
132
135
|
const p = localDateParts(date);
|
|
133
|
-
return join(rootFolder, String(p.year),
|
|
136
|
+
return join(rootFolder, String(p.year), getLocale(vaultBase).months[p.month - 1], `DIA ${pad2(p.day)}`);
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
// Mesma estrutura datada a partir de uma string 'YYYY-MM-DD' (sessões: até o DIA).
|
|
137
|
-
export function datedFolderRelFromDateStr(rootFolder, dateStr) {
|
|
140
|
+
export function datedFolderRelFromDateStr(rootFolder, dateStr, vaultBase) {
|
|
138
141
|
const [year, month, day] = String(dateStr).split('-');
|
|
139
|
-
return join(rootFolder, year,
|
|
142
|
+
return join(rootFolder, year, getLocale(vaultBase).months[Number(month) - 1], `DIA ${pad2(day)}`);
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// Estrutura até o MÊS (sem DIA) — usada pelas notas derivadas (decisões/bugs/
|
|
143
146
|
// aprendizados): tudo do mês fica junto em <pasta>/<ano>/<MM-MMM>/.
|
|
144
|
-
export function monthFolderRelFromDateStr(rootFolder, dateStr) {
|
|
147
|
+
export function monthFolderRelFromDateStr(rootFolder, dateStr, vaultBase) {
|
|
145
148
|
const [year, month] = String(dateStr).split('-');
|
|
146
|
-
return join(rootFolder, year,
|
|
149
|
+
return join(rootFolder, year, getLocale(vaultBase).months[Number(month) - 1]);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
export function sessionFolderRel(date = new Date()) {
|
|
150
|
-
return datedFolderRel(
|
|
152
|
+
export function sessionFolderRel(date = new Date(), vaultBase) {
|
|
153
|
+
return datedFolderRel(getLocale(vaultBase).folders.sessions, date, vaultBase);
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export function controlPath(vaultBase) {
|
|
@@ -532,7 +535,7 @@ export function listMarkdownFiles(dir) {
|
|
|
532
535
|
}
|
|
533
536
|
|
|
534
537
|
export function getNextAdrNumber(vaultBase) {
|
|
535
|
-
const decisionsDir = join(vaultBase,
|
|
538
|
+
const decisionsDir = join(vaultBase, getLocale(vaultBase).folders.decisions);
|
|
536
539
|
let max = 0;
|
|
537
540
|
// Varre recursivamente: os ADRs agora vivem em subpastas datadas (AAAA/MM-MMM/DIA DD).
|
|
538
541
|
const walk = (dir) => {
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -115,7 +115,7 @@ Sessão ainda em andamento.
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
118
|
-
const folderRel = sessionFolderRel(now);
|
|
118
|
+
const folderRel = sessionFolderRel(now, vaultBase);
|
|
119
119
|
const folderAbs = join(vaultBase, folderRel);
|
|
120
120
|
ensureDir(folderAbs);
|
|
121
121
|
|
package/hooks/session-start.mjs
CHANGED
|
@@ -113,7 +113,7 @@ Sessão ainda em andamento.
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
116
|
-
const folderRel = sessionFolderRel(now);
|
|
116
|
+
const folderRel = sessionFolderRel(now, vaultBase);
|
|
117
117
|
const folderAbs = join(vaultBase, folderRel);
|
|
118
118
|
ensureDir(folderAbs);
|
|
119
119
|
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { createLinkedNotes } from './linked-notes.mjs';
|
|
|
7
7
|
import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normalizeCodexUsage, priceForModel, updateSessionUsage } from './token-usage.mjs';
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
9
|
import { activeChangeLink } from './change-core.mjs';
|
|
10
|
+
import { getLocale } from './locale.mjs';
|
|
10
11
|
import {
|
|
11
12
|
ensureDir,
|
|
12
13
|
findActiveSessionByTranscript,
|
|
@@ -781,10 +782,11 @@ function noteReferencesSession(content, sessionRel) {
|
|
|
781
782
|
|
|
782
783
|
function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
783
784
|
const linked = { decisions: [], bugs: [], learnings: [] };
|
|
785
|
+
const locF = getLocale(vaultBase).folders;
|
|
784
786
|
const folders = {
|
|
785
|
-
decisions:
|
|
786
|
-
bugs:
|
|
787
|
-
learnings:
|
|
787
|
+
decisions: locF.decisions,
|
|
788
|
+
bugs: locF.bugs,
|
|
789
|
+
learnings: locF.learnings,
|
|
788
790
|
};
|
|
789
791
|
|
|
790
792
|
for (const [key, folder] of Object.entries(folders)) {
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -4,14 +4,15 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ensureDir } from './obsidian-common.mjs';
|
|
7
|
+
import { getLocale } from './locale.mjs';
|
|
7
8
|
|
|
8
9
|
// Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
|
|
9
10
|
export function tasksHashOf(md) {
|
|
10
11
|
return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
const REQ_RE = /^### Requisito:\s*(.+)$/gm;
|
|
14
|
+
// Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
|
|
15
|
+
const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
|
|
15
16
|
|
|
16
17
|
export function parseRequirements(md) {
|
|
17
18
|
const text = String(md);
|
|
@@ -71,8 +72,8 @@ export function applyDelta(reqs, delta) {
|
|
|
71
72
|
return { reqs: out, warnings };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
export function renderSpec(capability, reqs, { footer } = {}) {
|
|
75
|
-
const blocks = reqs.map((r) => `###
|
|
75
|
+
export function renderSpec(capability, reqs, { footer, reqHeading = 'Requisito' } = {}) {
|
|
76
|
+
const blocks = reqs.map((r) => `### ${reqHeading}: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
|
|
76
77
|
const foot = footer ? `\n\n> ${footer}\n` : '\n';
|
|
77
78
|
return `---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
|
|
78
79
|
}
|
|
@@ -88,20 +89,22 @@ export function parseSpecsList(propostaMd) {
|
|
|
88
89
|
|
|
89
90
|
// Merge each capability's delta (in the change) into the living spec in 07-Specs.
|
|
90
91
|
export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, dateStr } = {}) {
|
|
92
|
+
const loc = getLocale(vaultBase);
|
|
93
|
+
const specsDir = loc.folders.specs;
|
|
91
94
|
const promoted = [];
|
|
92
95
|
const warnings = [];
|
|
93
96
|
for (const cap of specs) {
|
|
94
97
|
let delta;
|
|
95
98
|
try { delta = parseDelta(readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8')); }
|
|
96
99
|
catch { warnings.push(`sem delta para ${cap}`); continue; }
|
|
97
|
-
const livePath = join(vaultBase,
|
|
100
|
+
const livePath = join(vaultBase, specsDir, `${cap}.md`);
|
|
98
101
|
let current = [];
|
|
99
102
|
try { current = parseRequirements(readFileSync(livePath, 'utf8')); } catch { /* nova capability */ }
|
|
100
103
|
const applied = applyDelta(current, delta);
|
|
101
104
|
warnings.push(...applied.warnings.map((w) => `${cap}: ${w}`));
|
|
102
|
-
ensureDir(join(vaultBase,
|
|
105
|
+
ensureDir(join(vaultBase, specsDir));
|
|
103
106
|
const footer = changeWikilink ? `Atualizado por ${changeWikilink} em ${dateStr}.` : '';
|
|
104
|
-
writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer }), 'utf8');
|
|
107
|
+
writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer, reqHeading: loc.reqHeading }), 'utf8');
|
|
105
108
|
promoted.push(cap);
|
|
106
109
|
}
|
|
107
110
|
return { promoted, warnings };
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readSessionRegistry,
|
|
11
11
|
wikilinkFromRel,
|
|
12
12
|
} from './obsidian-common.mjs';
|
|
13
|
+
import { getLocale } from './locale.mjs';
|
|
13
14
|
|
|
14
15
|
const DEFAULT_PENDING_PATTERNS = [
|
|
15
16
|
/^- \[ \] Revisar resumo da sessão$/i,
|
|
@@ -164,7 +165,8 @@ export function runVaultHealth({ vaultBase, session = '' }) {
|
|
|
164
165
|
.length;
|
|
165
166
|
if (staleDone) warnings.push(`${staleDone} entradas active com ended_at no SESSION_REGISTRY.`);
|
|
166
167
|
|
|
167
|
-
const
|
|
168
|
+
const locF = getLocale(vaultBase).folders;
|
|
169
|
+
const derivedFolders = [locF.decisions, locF.bugs, locF.learnings];
|
|
168
170
|
const derivedCount = derivedFolders.reduce((total, folder) => {
|
|
169
171
|
const dir = join(vaultBase, folder);
|
|
170
172
|
return total + (existsSync(dir) ? listMarkdownFiles(dir).length : 0);
|