wendkeep 0.33.0 → 0.34.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 +32 -0
- package/README.md +10 -9
- package/bin/wendkeep.mjs +5 -4
- package/hooks/change-core.mjs +36 -2
- package/hooks/harness-doctor.mjs +16 -32
- package/hooks/spec-core.mjs +163 -3
- package/package.json +1 -1
- package/src/change.mjs +47 -4
- package/src/doctor.mjs +6 -0
- package/src/init.mjs +10 -5
- package/src/skills-seed.mjs +26 -21
- package/src/spec.mjs +68 -2
- package/src/sync-defs.mjs +84 -14
- package/src/verify.mjs +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.34.0] — 2026-07-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Spec efetiva por change**: `wendkeep spec effective --change <slug> [--json]` combina o
|
|
12
|
+
contrato consolidado com somente o delta selecionado, incluindo origem e operação por requisito.
|
|
13
|
+
- `change use <slug>` troca o ponteiro global; `change continue <arquivada> <novo-slug>` cria
|
|
14
|
+
continuação com backlink sem reabrir arquivo nem herdar evidência/verdict.
|
|
15
|
+
- `sync-defs --check`, metadata de versão/hash e entrega idêntica de skills em `.claude/skills`
|
|
16
|
+
e `.agents/skills`.
|
|
17
|
+
- `SPECS_STATE.json`, baseline por change e `spec migrate/rebase` para detectar edição direta e
|
|
18
|
+
conflitos concorrentes no mesmo requisito.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `08-Mudanças/<slug>/specs/` é o único local de autoria. `07-Specs` permanece como contrato
|
|
23
|
+
consolidado gerado/read-only, preservando ADRs e links históricos.
|
|
24
|
+
- `verify --deep` agora grava requisitos completos em `verificacao.json` e sela o pacote com
|
|
25
|
+
`effectiveSpecHash`; `wk-verify` não relê `07-Specs`.
|
|
26
|
+
- `doctor` valida todas as changes abertas e avisa quando skills precisam de reseed + reinício.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Requisitos `ADDED` ainda não arquivados deixam de ficar invisíveis ao verificador independente.
|
|
31
|
+
- Archives concorrentes bloqueiam somente quando outra change alterou o mesmo requisito; mudanças
|
|
32
|
+
não relacionadas na mesma capability podem prosseguir.
|
|
33
|
+
|
|
34
|
+
### Migration
|
|
35
|
+
|
|
36
|
+
- Rode `wendkeep spec migrate --vault <vault>` uma vez para adotar os contratos consolidados atuais.
|
|
37
|
+
- Rode `wendkeep sync-defs --reseed --vault <vault> --project .` e reinicie Claude Code/Codex.
|
|
38
|
+
|
|
7
39
|
## [0.33.0] — 2026-07-11
|
|
8
40
|
|
|
9
41
|
### Added
|
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx wendkeep init
|
|
|
71
71
|
Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
|
|
72
72
|
5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
|
|
73
73
|
6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
74
|
-
7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify
|
|
74
|
+
7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify`. `init` delivers them to `.codex/agents/`, `.claude/skills/`, and `.agents/skills/`; `sync-defs --check` detects stale copies.
|
|
75
75
|
8. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` (a `validate-memory` sensor plus one per detected `package.json` script). Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
|
|
76
76
|
|
|
77
77
|
```bash
|
|
@@ -114,12 +114,12 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
114
114
|
| `wendkeep hook <name>` | Run a session hook; invoked by `settings.json` (reads agent JSON on stdin). |
|
|
115
115
|
| `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` (global backlog) / `show` / `status [slug]` / `done <id> [--change slug]` / `undone <id> [--change slug]` / `diff` / `archive [--force]`. |
|
|
116
116
|
| `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
|
|
117
|
-
| `wendkeep spec <sub>` |
|
|
117
|
+
| `wendkeep spec <sub>` | `list` / `show` generated contracts; `effective --change <slug>`; `migrate`; `rebase`. |
|
|
118
118
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
|
|
119
119
|
| `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
|
|
120
120
|
| `wendkeep import [opts]` | **Retroactive memory** — backfill past **Claude + Codex** sessions into the vault (deduped by `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
|
|
121
121
|
| `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
122
|
-
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into
|
|
122
|
+
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
|
|
123
123
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
|
124
124
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
125
125
|
| `wendkeep --version` / `--help` | Version / usage. |
|
|
@@ -148,14 +148,14 @@ Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependenc
|
|
|
148
148
|
explore → propose → apply (TDD) → verify → archive
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
- **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). It becomes the global *current* change. Multiple changes may remain open
|
|
151
|
+
- **Propose** — `wendkeep change new <slug>` scaffolds `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md`, and a `specs/` delta). It becomes the global *current* change. Multiple changes may remain open; `change use <slug>` changes focus and `change continue <archived> <new>` creates an auditable continuation.
|
|
152
152
|
- **Apply** — implement each `tarefas.md` task. Tag a task that needs machine proof with `[sensor:<id>]`.
|
|
153
|
-
- **Verify** — `wendkeep verify`
|
|
153
|
+
- **Verify** — `wendkeep verify` records sensor evidence. `verify --deep` builds a self-contained package with complete effective requirements (living contract + this change's delta), so the independent verifier never needs to reconstruct unarchived requirements from `07-Specs`.
|
|
154
154
|
- **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/`.
|
|
155
155
|
|
|
156
156
|
> The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` is the human's call — never the agent's.**
|
|
157
157
|
|
|
158
|
-
`wendkeep init`
|
|
158
|
+
`wendkeep init` seeds process skills into `.brain/skills` and delivers identical copies to Claude Code and Codex. Every skill carries source hash/version metadata; `doctor` warns when reseed + agent restart is required.
|
|
159
159
|
|
|
160
160
|
### The loop in five minutes
|
|
161
161
|
|
|
@@ -170,13 +170,14 @@ Edit `tarefas.md` — tag proof and requirement per task:
|
|
|
170
170
|
- [ ] 1.1 toggle persists across sessions [req:UI-1] [sensor:tests]
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta in
|
|
174
|
-
`specs/ui/spec.md
|
|
173
|
+
Declare the capability in `proposta.md` (`specs: [ui]`) and author its delta only in
|
|
174
|
+
`08-Mudanças/<slug>/specs/ui/spec.md`. `07-Specs` is generated/read-only. Then:
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
npx wendkeep change status # one screen: tasks / sensors / verdict
|
|
178
178
|
npx wendkeep change list # all open changes + pending tasks
|
|
179
179
|
npx wendkeep change status dark-mode # one change in detail
|
|
180
|
+
npx wendkeep spec effective --change dark-mode # living contract + this change's delta
|
|
180
181
|
npx wendkeep change done 1.1 # tick a task from the CLI
|
|
181
182
|
npx wendkeep verify # run the declared sensors -> evidencia.json
|
|
182
183
|
npx wendkeep verify --deep # assemble the verification package
|
|
@@ -185,7 +186,7 @@ npx wendkeep change diff # preview what will land in 07-Sp
|
|
|
185
186
|
npx wendkeep change archive dark-mode # gate: sensors + verdict + no open tasks
|
|
186
187
|
```
|
|
187
188
|
|
|
188
|
-
The archive promotes the delta into
|
|
189
|
+
The archive promotes the delta into generated `07-Specs/ui.md`, mints an ADR, and the
|
|
189
190
|
Obsidian graph now links *session ↔ change ↔ requirement ↔ decision*. A change that names
|
|
190
191
|
no `[req:]` skips the independent verdict — the sensor gate is its proof.
|
|
191
192
|
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -43,10 +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 [--simple] |
|
|
47
|
-
done <id> | undone <id> | diff | archive [--force] | abandon
|
|
46
|
+
wendkeep change <sub> Change lifecycle: new [--simple] | use | continue | list | show |
|
|
47
|
+
status | done <id> | undone <id> | diff | archive [--force] | abandon.
|
|
48
48
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
49
|
-
wendkeep spec <sub>
|
|
49
|
+
wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
|
|
50
50
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
51
51
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
52
52
|
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
@@ -70,9 +70,10 @@ Usage:
|
|
|
70
70
|
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
71
71
|
wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
|
|
72
72
|
project: .brain/agents/*.toml -> .codex/agents,
|
|
73
|
-
.brain/skills/<name> -> .claude/skills. --vault P --project P.
|
|
73
|
+
.brain/skills/<name> -> .claude/skills + .agents/skills. --vault P --project P.
|
|
74
74
|
--reseed re-semeia as skills wk-* com os seeds da versão instalada
|
|
75
75
|
(sobrescreve edições manuais nas wk-*) antes de copiar.
|
|
76
|
+
--check detecta drift sem modificar arquivos.
|
|
76
77
|
wendkeep --version Print version.
|
|
77
78
|
wendkeep --help Show this help.
|
|
78
79
|
`;
|
package/hooks/change-core.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
|
-
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf } from './spec-core.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
10
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
@@ -119,10 +119,44 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
119
119
|
writeFileSync(exampleDelta, files.specDelta, 'utf8');
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
if (!existed) captureSpecBaseline(vaultBase, dir);
|
|
122
123
|
setActiveChange(vaultBase, slug);
|
|
123
124
|
return { rel: changeDirRel(slug, vaultBase), created: !existed };
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export function useChange(vaultBase, slug) {
|
|
128
|
+
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
129
|
+
if (!existsSync(join(dir, 'proposta.md'))) return { ok: false, error: `change aberta não encontrada: ${slug}` };
|
|
130
|
+
setActiveChange(vaultBase, slug);
|
|
131
|
+
return { ok: true, rel: changeDirRel(slug, vaultBase) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function continueChange(vaultBase, archivedSlug, newSlug, options = {}) {
|
|
135
|
+
const loc = getLocale(vaultBase);
|
|
136
|
+
const archiveDir = join(vaultBase, loc.folders.changes, ARCHIVE_DIR);
|
|
137
|
+
let names = [];
|
|
138
|
+
try { names = readdirSync(archiveDir).filter((name) => statSync(join(archiveDir, name)).isDirectory()); } catch { /* no archive */ }
|
|
139
|
+
const matches = names.filter((name) => name === archivedSlug || name.endsWith(`-${archivedSlug}`));
|
|
140
|
+
if (!matches.length) return { ok: false, error: `change arquivada não encontrada: ${archivedSlug}` };
|
|
141
|
+
if (matches.length > 1) return { ok: false, error: `change arquivada ambígua: ${archivedSlug} (${matches.join(', ')})` };
|
|
142
|
+
if (existsSync(join(vaultBase, loc.folders.changes, newSlug, 'proposta.md'))) {
|
|
143
|
+
return { ok: false, error: `change de continuação já existe: ${newSlug}` };
|
|
144
|
+
}
|
|
145
|
+
const archivedName = matches[0];
|
|
146
|
+
const archivedProposal = join(loc.folders.changes, ARCHIVE_DIR, archivedName, 'proposta');
|
|
147
|
+
const result = newChange(vaultBase, newSlug, options);
|
|
148
|
+
const proposalPath = join(vaultBase, loc.folders.changes, newSlug, 'proposta.md');
|
|
149
|
+
let proposal = readFileSync(proposalPath, 'utf8');
|
|
150
|
+
proposal = proposal.replace(/^specs:/m, `continues: "${wikilinkFromRel(archivedProposal)}"\nspecs:`);
|
|
151
|
+
const heading = loc.id === 'en' ? '## Continuation' : '## Continuação';
|
|
152
|
+
const note = loc.id === 'en'
|
|
153
|
+
? `Continues ${wikilinkFromRel(archivedProposal)}. Archived evidence and verdict are not inherited.`
|
|
154
|
+
: `Continua ${wikilinkFromRel(archivedProposal)}. Evidências e verdict da change arquivada não são herdados.`;
|
|
155
|
+
proposal = `${proposal.trimEnd()}\n\n${heading}\n\n${note}\n`;
|
|
156
|
+
writeFileSync(proposalPath, proposal, 'utf8');
|
|
157
|
+
return { ok: true, ...result, archived: archivedName };
|
|
158
|
+
}
|
|
159
|
+
|
|
126
160
|
export function parseTasks(md) {
|
|
127
161
|
const tasks = [];
|
|
128
162
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
@@ -219,7 +253,7 @@ export function renderOpenChanges(state, { tag = 'open_changes' } = {}) {
|
|
|
219
253
|
else for (const task of change.openTasks) lines.push(`- [ ] ${task.id} ${task.text}`);
|
|
220
254
|
}
|
|
221
255
|
if (state.current) lines.push('Para change atual: `wendkeep change done <id>`; antes de archive: `wendkeep verify`.');
|
|
222
|
-
lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change
|
|
256
|
+
lines.push('Qualquer agente pode assumir uma change: selecione-a com `wendkeep change use <slug>` ou use `--change <slug>` quando disponível.');
|
|
223
257
|
if (tag) lines.push(`</${tag}>`);
|
|
224
258
|
return lines.join('\n');
|
|
225
259
|
}
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { activeChange, parseTasks } from './change-core.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tasksHashOf, validateSpecImpact } from './spec-core.mjs';
|
|
7
7
|
import { getLocale } from './locale.mjs';
|
|
8
8
|
|
|
9
9
|
export function checkHarness(vaultBase, projectRoot) {
|
|
10
10
|
const loc = getLocale(vaultBase);
|
|
11
11
|
const CHANGES_DIR = loc.folders.changes;
|
|
12
|
-
const SPECS_DIR = loc.folders.specs;
|
|
13
12
|
const errors = [];
|
|
14
13
|
const warnings = [];
|
|
15
14
|
|
|
@@ -23,16 +22,9 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
23
22
|
} catch { errors.push('wendkeep.sensors.json: JSON inválido'); }
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
for (const f of readdirSync(join(vaultBase, SPECS_DIR))) {
|
|
30
|
-
if (!f.endsWith('.md') || f === 'README.md') continue;
|
|
31
|
-
const md = readFileSync(join(vaultBase, SPECS_DIR, f), 'utf8');
|
|
32
|
-
for (const r of parseRequirements(md)) if (r.id) knownReqs.add(r.id);
|
|
33
|
-
if (!/^>\s+Atualizado por/m.test(md)) warnings.push(`spec sem origem: ${SPECS_DIR}/${f}`);
|
|
34
|
-
}
|
|
35
|
-
} catch { /* sem 07-Specs */ }
|
|
25
|
+
const specState = checkSpecsState(vaultBase);
|
|
26
|
+
if (specState.missing) warnings.push('SPECS_STATE ausente — rode `wendkeep spec migrate`; 07-Specs deve ser gerado/read-only');
|
|
27
|
+
else if (!specState.ok) errors.push(`07-Specs alterado fora do WendKeep: ${specState.changed.join(', ')} — mova a alteração para 08-Mudanças/<change>/specs`);
|
|
36
28
|
|
|
37
29
|
// 2/3. Changes: malformed dirs; the active change's deltas add to knownReqs.
|
|
38
30
|
const active = activeChange(vaultBase);
|
|
@@ -46,15 +38,18 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
46
38
|
const impact = validateSpecImpact(dir);
|
|
47
39
|
errors.push(...impact.errors.map((e) => `${name}: ${e}`));
|
|
48
40
|
warnings.push(...impact.warnings.map((w) => `${name}: ${w}`));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
let tasks = [];
|
|
42
|
+
let tarefasMd = '';
|
|
43
|
+
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
|
|
44
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
45
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
46
|
+
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
47
|
+
if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
|
|
48
|
+
let verdict = null;
|
|
49
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
50
|
+
if (verdict && reqIds.length) {
|
|
51
|
+
const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
|
|
52
|
+
if (!v.ok) warnings.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
55
|
|
|
@@ -63,17 +58,6 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
63
58
|
const dir = join(vaultBase, CHANGES_DIR, active);
|
|
64
59
|
if (!existsSync(join(dir, 'proposta.md'))) {
|
|
65
60
|
errors.push(`ponteiro CURRENT_CHANGE aponta pra change inexistente: ${active}`);
|
|
66
|
-
} else {
|
|
67
|
-
let tasks = [];
|
|
68
|
-
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
|
|
69
|
-
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
70
|
-
for (const rid of reqIds) if (!knownReqs.has(rid)) errors.push(`req órfão em ${active}: [req:${rid}] não existe em 07-Specs nem no delta`);
|
|
71
|
-
let verdict = null;
|
|
72
|
-
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
73
|
-
if (verdict && reqIds.length) {
|
|
74
|
-
const v = evaluateVerdict(verdict, reqIds);
|
|
75
|
-
if (!v.ok) warnings.push(`verdict stale em ${active}: falta cobrir ${v.missing.join(', ')}`);
|
|
76
|
-
}
|
|
77
61
|
}
|
|
78
62
|
}
|
|
79
63
|
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -11,6 +11,14 @@ export function tasksHashOf(md) {
|
|
|
11
11
|
return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function contentHashOf(value) {
|
|
15
|
+
return createHash('sha256').update(String(value)).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SPECS_STATE_FILE = '.brain/SPECS_STATE.json';
|
|
19
|
+
export const SPEC_BASELINE_FILE = '.spec-base.json';
|
|
20
|
+
export const MANAGED_SPEC_MARKER = '<!-- wendkeep:managed-spec — generated from 08-Mudanças; do not edit directly -->';
|
|
21
|
+
|
|
14
22
|
// Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
|
|
15
23
|
const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
|
|
16
24
|
|
|
@@ -21,7 +29,7 @@ export function parseRequirements(md) {
|
|
|
21
29
|
for (let i = 0; i < matches.length; i += 1) {
|
|
22
30
|
const raw = matches[i][1].trim();
|
|
23
31
|
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"; else the whole text.
|
|
24
|
-
const idM = raw.match(/^([A-Z][A-Z0-9]*-\d+)\s*—\s*(.+)$/);
|
|
32
|
+
const idM = raw.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+)\s*—\s*(.+)$/);
|
|
25
33
|
const id = idM ? idM[1] : null;
|
|
26
34
|
const name = idM ? idM[2].trim() : raw;
|
|
27
35
|
const start = matches[i].index + matches[i][0].length;
|
|
@@ -75,7 +83,148 @@ export function applyDelta(reqs, delta) {
|
|
|
75
83
|
export function renderSpec(capability, reqs, { footer, reqHeading = 'Requisito' } = {}) {
|
|
76
84
|
const blocks = reqs.map((r) => `### ${reqHeading}: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
|
|
77
85
|
const foot = footer ? `\n\n> ${footer}\n` : '\n';
|
|
78
|
-
return
|
|
86
|
+
return `${MANAGED_SPEC_MARKER}\n---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readLivingSpecs(vaultBase) {
|
|
90
|
+
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
91
|
+
const specs = {};
|
|
92
|
+
let files = [];
|
|
93
|
+
try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { return specs; }
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const capability = file.replace(/\.md$/, '');
|
|
96
|
+
const md = readFileSync(join(specsDir, file), 'utf8');
|
|
97
|
+
specs[capability] = {
|
|
98
|
+
hash: contentHashOf(md),
|
|
99
|
+
requirements: Object.fromEntries(parseRequirements(md).map((r) => [r.id || r.name, contentHashOf(JSON.stringify(r))])),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return specs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function livingSpecCapabilities(vaultBase) {
|
|
106
|
+
return Object.keys(readLivingSpecs(vaultBase));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function adoptSpecsState(vaultBase) {
|
|
110
|
+
const state = { version: 1, generatedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
|
|
111
|
+
ensureDir(join(vaultBase, '.brain'));
|
|
112
|
+
writeFileSync(join(vaultBase, SPECS_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function readSpecsState(vaultBase) {
|
|
117
|
+
try { return JSON.parse(readFileSync(join(vaultBase, SPECS_STATE_FILE), 'utf8')); } catch { return null; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function checkSpecsState(vaultBase) {
|
|
121
|
+
const recorded = readSpecsState(vaultBase);
|
|
122
|
+
if (!recorded) return { ok: false, missing: true, changed: [] };
|
|
123
|
+
const current = readLivingSpecs(vaultBase);
|
|
124
|
+
const names = new Set([...Object.keys(recorded.specs || {}), ...Object.keys(current)]);
|
|
125
|
+
const changed = [...names].filter((name) => recorded.specs?.[name]?.hash !== current[name]?.hash);
|
|
126
|
+
return { ok: changed.length === 0, missing: false, changed, current, recorded };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function recordPromotedSpecs(vaultBase, capabilities) {
|
|
130
|
+
const existing = readSpecsState(vaultBase);
|
|
131
|
+
if (!existing) return adoptSpecsState(vaultBase);
|
|
132
|
+
const current = readLivingSpecs(vaultBase);
|
|
133
|
+
const specs = { ...(existing.specs || {}) };
|
|
134
|
+
for (const capability of capabilities) {
|
|
135
|
+
if (current[capability]) specs[capability] = current[capability];
|
|
136
|
+
else delete specs[capability];
|
|
137
|
+
}
|
|
138
|
+
const state = { version: 1, generatedAt: new Date().toISOString(), specs };
|
|
139
|
+
writeFileSync(join(vaultBase, SPECS_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
140
|
+
return state;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } = {}) {
|
|
144
|
+
const path = join(changeDir, SPEC_BASELINE_FILE);
|
|
145
|
+
if (!refresh && existsSync(path)) {
|
|
146
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
|
|
147
|
+
}
|
|
148
|
+
const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
|
|
149
|
+
writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
|
|
150
|
+
return baseline;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function specConflicts(vaultBase, changeDir, capabilities = discoverSpecDeltas(changeDir)) {
|
|
154
|
+
let baseline = null;
|
|
155
|
+
try { baseline = JSON.parse(readFileSync(join(changeDir, SPEC_BASELINE_FILE), 'utf8')); } catch { /* legacy change */ }
|
|
156
|
+
if (!baseline) return [];
|
|
157
|
+
const current = readLivingSpecs(vaultBase);
|
|
158
|
+
const conflicts = [];
|
|
159
|
+
for (const capability of capabilities) {
|
|
160
|
+
let delta;
|
|
161
|
+
try { delta = parseDelta(readFileSync(join(changeDir, 'specs', capability, 'spec.md'), 'utf8')); } catch { continue; }
|
|
162
|
+
const baseReqs = baseline.specs?.[capability]?.requirements || {};
|
|
163
|
+
const currentReqs = current[capability]?.requirements || {};
|
|
164
|
+
for (const req of delta.added || []) {
|
|
165
|
+
const key = req.id || req.name;
|
|
166
|
+
if (!(key in baseReqs) && key in currentReqs) conflicts.push(`${capability}:${key} foi adicionado por outra change`);
|
|
167
|
+
}
|
|
168
|
+
for (const req of delta.modified || []) {
|
|
169
|
+
const key = req.id || req.name;
|
|
170
|
+
if (baseReqs[key] !== currentReqs[key]) conflicts.push(`${capability}:${key} mudou desde a abertura da change`);
|
|
171
|
+
}
|
|
172
|
+
for (const key of delta.removed || []) {
|
|
173
|
+
if (baseReqs[key] !== currentReqs[key]) conflicts.push(`${capability}:${key} mudou desde a abertura da change`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return conflicts;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveEffectiveSpecs(vaultBase, changeDir, capabilities = discoverSpecDeltas(changeDir)) {
|
|
180
|
+
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
181
|
+
const result = [];
|
|
182
|
+
const warnings = [];
|
|
183
|
+
const errors = [];
|
|
184
|
+
for (const capability of capabilities) {
|
|
185
|
+
let living = [];
|
|
186
|
+
try { living = parseRequirements(readFileSync(join(specsDir, `${capability}.md`), 'utf8')); } catch { /* new capability */ }
|
|
187
|
+
let deltaMd = '';
|
|
188
|
+
try { deltaMd = readFileSync(join(changeDir, 'specs', capability, 'spec.md'), 'utf8'); } catch { /* unchanged living capability */ }
|
|
189
|
+
if (deltaMd && isPlaceholderDelta(deltaMd)) deltaMd = '';
|
|
190
|
+
const delta = deltaMd ? parseDelta(deltaMd) : { added: [], modified: [], removed: [] };
|
|
191
|
+
const operations = new Map(living.map((r) => [r.id || r.name, { operation: 'BASE', source: 'living' }]));
|
|
192
|
+
for (const r of delta.added) operations.set(r.id || r.name, { operation: 'ADDED', source: 'change' });
|
|
193
|
+
for (const r of delta.modified) operations.set(r.id || r.name, { operation: 'MODIFIED', source: 'change' });
|
|
194
|
+
for (const key of delta.removed) operations.delete(key);
|
|
195
|
+
const applied = applyDelta(living, delta);
|
|
196
|
+
warnings.push(...applied.warnings.map((w) => `${capability}: ${w}`));
|
|
197
|
+
for (const warning of applied.warnings) {
|
|
198
|
+
if (/^(ADDED já existe|MODIFIED inexistente|REMOVED inexistente)/.test(warning)) errors.push(`${capability}: ${warning}`);
|
|
199
|
+
}
|
|
200
|
+
result.push({
|
|
201
|
+
capability,
|
|
202
|
+
requirements: applied.reqs.map((r) => ({ ...r, capability, ...(operations.get(r.id || r.name) || { operation: 'BASE', source: 'living' }) })),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const serializable = result.map((s) => ({ capability: s.capability, requirements: s.requirements }));
|
|
206
|
+
return { specs: result, requirements: result.flatMap((s) => s.requirements), warnings, errors, hash: contentHashOf(JSON.stringify(serializable)) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds = []) {
|
|
210
|
+
let listed = [];
|
|
211
|
+
try { listed = parseSpecsList(readFileSync(join(changeDir, 'proposta.md'), 'utf8')); } catch { /* caller validates change */ }
|
|
212
|
+
const changed = [...new Set([...listed, ...discoverSpecDeltas(changeDir)])];
|
|
213
|
+
const capabilities = [...new Set([...livingSpecCapabilities(vaultBase), ...changed])];
|
|
214
|
+
const effective = resolveEffectiveSpecs(vaultBase, changeDir, capabilities);
|
|
215
|
+
const byId = new Map(effective.requirements.filter((r) => r.id).map((r) => [r.id, r]));
|
|
216
|
+
const missing = reqIds.filter((id) => !byId.has(id));
|
|
217
|
+
const requirements = reqIds.map((id) => byId.get(id)).filter(Boolean);
|
|
218
|
+
const relevantCaps = new Set([...changed, ...requirements.map((r) => r.capability)]);
|
|
219
|
+
const relevant = effective.specs.filter((spec) => relevantCaps.has(spec.capability));
|
|
220
|
+
return {
|
|
221
|
+
...effective,
|
|
222
|
+
specs: relevant,
|
|
223
|
+
requirements,
|
|
224
|
+
missing,
|
|
225
|
+
changedCapabilities: changed,
|
|
226
|
+
hash: contentHashOf(JSON.stringify(relevant)),
|
|
227
|
+
};
|
|
79
228
|
}
|
|
80
229
|
|
|
81
230
|
export function parseSpecsList(propostaMd) {
|
|
@@ -160,6 +309,13 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
160
309
|
const specsDir = loc.folders.specs;
|
|
161
310
|
const promoted = [];
|
|
162
311
|
const warnings = [];
|
|
312
|
+
const state = checkSpecsState(vaultBase);
|
|
313
|
+
const unmanaged = state.missing ? [] : state.changed.filter((capability) => specs.includes(capability));
|
|
314
|
+
if (unmanaged.length) {
|
|
315
|
+
throw new Error(`07-Specs alterado fora do WendKeep: ${unmanaged.join(', ')} — mova o delta para 08-Mudanças/<change>/specs`);
|
|
316
|
+
}
|
|
317
|
+
const conflicts = specConflicts(vaultBase, changeDir, specs);
|
|
318
|
+
if (conflicts.length) throw new Error(`conflito de spec: ${conflicts.join('; ')} — reconcilie o delta e rode \`wendkeep spec rebase --change <slug> --accept-current\``);
|
|
163
319
|
for (const cap of specs) {
|
|
164
320
|
let deltaMd;
|
|
165
321
|
try { deltaMd = readFileSync(join(changeDir, 'specs', cap, 'spec.md'), 'utf8'); }
|
|
@@ -176,13 +332,14 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
176
332
|
writeFileSync(livePath, renderSpec(cap, applied.reqs, { footer, reqHeading: loc.reqHeading }), 'utf8');
|
|
177
333
|
promoted.push(cap);
|
|
178
334
|
}
|
|
335
|
+
recordPromotedSpecs(vaultBase, promoted);
|
|
179
336
|
return { promoted, warnings };
|
|
180
337
|
}
|
|
181
338
|
|
|
182
339
|
// Gate check for the independent verdict (Wave A). A requirement-bearing change must have
|
|
183
340
|
// a verdict that is ok and covers every declared req id. A requirement-less change passes:
|
|
184
341
|
// nothing for an independent verifier to check — the sensor gate is already the proof.
|
|
185
|
-
export function evaluateVerdict(verdict, reqIds, { tasksHash } = {}) {
|
|
342
|
+
export function evaluateVerdict(verdict, reqIds, { tasksHash, effectiveSpecHash } = {}) {
|
|
186
343
|
const ids = reqIds || [];
|
|
187
344
|
if (ids.length === 0) return { ok: true, missing: [] };
|
|
188
345
|
if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
|
|
@@ -191,6 +348,9 @@ export function evaluateVerdict(verdict, reqIds, { tasksHash } = {}) {
|
|
|
191
348
|
if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
|
|
192
349
|
return { ok: false, missing: [], stale: true };
|
|
193
350
|
}
|
|
351
|
+
if (effectiveSpecHash && verdict.effectiveSpecHash && verdict.effectiveSpecHash !== effectiveSpecHash) {
|
|
352
|
+
return { ok: false, missing: [], stale: true };
|
|
353
|
+
}
|
|
194
354
|
const covered = new Set((verdict.coverage || []).filter((c) => c.covered).map((c) => c.req));
|
|
195
355
|
const missing = ids.filter((r) => !covered.has(r));
|
|
196
356
|
return { ok: missing.length === 0, missing };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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/change.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import {
|
|
5
5
|
newChange,
|
|
6
|
+
useChange,
|
|
7
|
+
continueChange,
|
|
6
8
|
activeChange,
|
|
7
9
|
allChangesState,
|
|
8
10
|
listChanges,
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
scaffoldPlaceholders,
|
|
15
17
|
} from '../hooks/change-core.mjs';
|
|
16
18
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
17
|
-
import { evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
19
|
+
import { buildEffectiveRequirementPackage, evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
18
20
|
import { getNextAdrNumber, readControl } from '../hooks/obsidian-common.mjs';
|
|
19
21
|
import { getLocale } from '../hooks/locale.mjs';
|
|
20
22
|
|
|
@@ -62,6 +64,32 @@ export function runChange(argv) {
|
|
|
62
64
|
process.exit(0);
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
if (sub === 'use') {
|
|
68
|
+
const slug = slugArg();
|
|
69
|
+
if (!slug) { process.stderr.write('wendkeep change use: missing <slug>\n'); process.exit(2); }
|
|
70
|
+
const r = useChange(vaultBase, slug);
|
|
71
|
+
if (!r.ok) { process.stderr.write(`wendkeep change use: ${r.error}\n`); process.exit(2); }
|
|
72
|
+
process.stdout.write(`current change: ${slug}\n`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sub === 'continue') {
|
|
77
|
+
const positionals = rest.filter((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
78
|
+
const [archivedSlug, newSlug] = positionals;
|
|
79
|
+
if (!archivedSlug || !newSlug) {
|
|
80
|
+
process.stderr.write('wendkeep change continue: use <archived-slug> <new-slug>\n');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
let sessionRel = '';
|
|
84
|
+
try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* no control */ }
|
|
85
|
+
const r = continueChange(vaultBase, archivedSlug, newSlug, {
|
|
86
|
+
dateStr: today(), simple: rest.includes('--simple'), sessionRel,
|
|
87
|
+
});
|
|
88
|
+
if (!r.ok) { process.stderr.write(`wendkeep change continue: ${r.error}\n`); process.exit(2); }
|
|
89
|
+
process.stdout.write(`change created: ${r.rel} (continues ${r.archived}; active)\n`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
65
93
|
if (sub === 'list') {
|
|
66
94
|
const state = allChangesState(vaultBase);
|
|
67
95
|
const { archived } = listChanges(vaultBase);
|
|
@@ -113,12 +141,16 @@ export function runChange(argv) {
|
|
|
113
141
|
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
114
142
|
else process.stdout.write('evidencia: ausente\n');
|
|
115
143
|
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
144
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
145
|
+
if (effective.errors.length || effective.missing.length) {
|
|
146
|
+
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
147
|
+
}
|
|
116
148
|
let verdict = null;
|
|
117
149
|
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
118
150
|
if (!verdict) process.stdout.write(`verdict: ausente — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ' (verdict trivial automático)'}\n`);
|
|
119
151
|
else if (!reqIds.length) process.stdout.write(`verdict: ${verdict.ok === true ? 'ok (trivial)' : 'não-ok — re-verifique'}\n`);
|
|
120
152
|
else {
|
|
121
|
-
const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd) });
|
|
153
|
+
const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
|
|
122
154
|
process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
|
|
123
155
|
}
|
|
124
156
|
try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
|
|
@@ -195,6 +227,9 @@ export function runChange(argv) {
|
|
|
195
227
|
}
|
|
196
228
|
}
|
|
197
229
|
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
230
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
231
|
+
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
232
|
+
if (effective.missing.length) return { ok: false, failing: [`requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}`] };
|
|
198
233
|
let evidence = [];
|
|
199
234
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
200
235
|
const s = evaluateGate(evidence, required);
|
|
@@ -214,8 +249,16 @@ export function runChange(argv) {
|
|
|
214
249
|
if (verdict.tasksHash && verdict.tasksHash !== hash) {
|
|
215
250
|
return { ok: false, failing: [`verdict stale (tarefas.md mudou depois da verificação) — re-verifique: \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
|
|
216
251
|
}
|
|
252
|
+
let verification = null;
|
|
253
|
+
try { verification = JSON.parse(readFileSync(join(dir, 'verificacao.json'), 'utf8')); } catch { /* none */ }
|
|
254
|
+
if (verification?.effectiveSpecHash && verification.effectiveSpecHash !== effective.hash) {
|
|
255
|
+
return { ok: false, failing: ['pacote de verificação stale (spec efetiva mudou) — rode `wendkeep verify --deep` novamente'] };
|
|
256
|
+
}
|
|
257
|
+
if (reqIds.length && verification?.effectiveSpecHash && !verdict.effectiveSpecHash) {
|
|
258
|
+
return { ok: false, failing: ['verdict sem effectiveSpecHash — rode a skill wk-verify novamente'] };
|
|
259
|
+
}
|
|
217
260
|
if (reqIds.length) {
|
|
218
|
-
const v = evaluateVerdict(verdict, reqIds, { tasksHash: hash });
|
|
261
|
+
const v = evaluateVerdict(verdict, reqIds, { tasksHash: hash, effectiveSpecHash: effective.hash });
|
|
219
262
|
if (!v.ok) {
|
|
220
263
|
if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
|
|
221
264
|
return { ok: false, failing: [`verdict incompleto: falta ${v.missing.join(', ')}`] };
|
|
@@ -250,6 +293,6 @@ export function runChange(argv) {
|
|
|
250
293
|
process.exit(0);
|
|
251
294
|
}
|
|
252
295
|
|
|
253
|
-
process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, list, show, status, done, undone, diff, archive, abandon.\n`);
|
|
296
|
+
process.stderr.write(`wendkeep change: unknown subcommand "${sub}". Known: new, use, continue, list, show, status, done, undone, diff, archive, abandon.\n`);
|
|
254
297
|
process.exit(2);
|
|
255
298
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { checkHarness } from '../hooks/harness-doctor.mjs';
|
|
8
|
+
import { checkSyncDefs } from './sync-defs.mjs';
|
|
8
9
|
|
|
9
10
|
export function runDoctor(argv) {
|
|
10
11
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -40,6 +41,11 @@ export function runDoctor(argv) {
|
|
|
40
41
|
|
|
41
42
|
// 2. Harness integrity (Wave B).
|
|
42
43
|
const { errors, warnings } = checkHarness(vaultBase, projectRoot);
|
|
44
|
+
const defs = checkSyncDefs(vaultBase, projectRoot);
|
|
45
|
+
if (!defs.ok) {
|
|
46
|
+
warnings.push(...defs.issues.map((issue) => `defs: ${issue}`));
|
|
47
|
+
warnings.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
|
|
48
|
+
}
|
|
43
49
|
process.stdout.write(`\n[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\n`);
|
|
44
50
|
for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
|
|
45
51
|
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
package/src/init.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// OBSIDIAN_VAULT_PATH into .claude/settings.json, and adds the mcpvault server to
|
|
4
4
|
// .mcp.json. Idempotent: re-running only adds what is missing.
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
8
8
|
import { createInterface } from 'node:readline/promises';
|
|
9
9
|
import {
|
|
@@ -40,6 +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
44
|
|
|
44
45
|
function parseArgs(argv) {
|
|
45
46
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -278,7 +279,7 @@ const MESSAGES = {
|
|
|
278
279
|
colorsOn: 'wendkeep-colors (snippet + grupos do grafo)',
|
|
279
280
|
taxonomy: (n, c, loc, readme, views) => ` [1/4] taxonomia do vault: ${n} pastas (${c} criadas, locale ${loc})${readme}, .brain + change/spec + sensores semeados${views}`,
|
|
280
281
|
readmeCreated: ', README.md criado', viewsNote: (n) => `, ${n} view(s) + dashboard`,
|
|
281
|
-
defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
|
|
282
|
+
defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
|
|
282
283
|
settingsBadJson: (p) => ` [2/4] settings.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
|
|
283
284
|
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wirados, OBSIDIAN_VAULT_PATH setado${bak})`,
|
|
284
285
|
mcpBadJson: (p) => ` [3/4] .mcp.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
|
|
@@ -303,7 +304,7 @@ const MESSAGES = {
|
|
|
303
304
|
colorsOn: 'wendkeep-colors (snippet + graph groups)',
|
|
304
305
|
taxonomy: (n, c, loc, readme, views) => ` [1/4] vault taxonomy: ${n} folders (${c} created, locale ${loc})${readme}, .brain + change/spec + sensors seeded${views}`,
|
|
305
306
|
readmeCreated: ', README.md created', viewsNote: (n) => `, ${n} view(s) + dashboard`,
|
|
306
|
-
defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
|
|
307
|
+
defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills + .agents/skills, ${a} agent(s) -> .codex/agents`,
|
|
307
308
|
settingsBadJson: (p) => ` [2/4] settings.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
308
309
|
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${bak})`,
|
|
309
310
|
mcpBadJson: (p) => ` [3/4] .mcp.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
@@ -482,8 +483,12 @@ export async function runInit(argv) {
|
|
|
482
483
|
const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
|
|
483
484
|
if (!existsSync(specsReadme)) {
|
|
484
485
|
writeFileSync(specsReadme, en
|
|
485
|
-
? `# Specs — living contract\n\
|
|
486
|
-
: `# Specs — contrato
|
|
486
|
+
? `# 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`
|
|
487
|
+
: `# 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');
|
|
488
|
+
}
|
|
489
|
+
if (!existsSync(join(vaultPath, SPECS_STATE_FILE))) {
|
|
490
|
+
const livingSpecs = readdirSync(join(vaultPath, loc.folders.specs)).filter((name) => name.endsWith('.md') && name !== 'README.md');
|
|
491
|
+
if (!livingSpecs.length) adoptSpecsState(vaultPath);
|
|
487
492
|
}
|
|
488
493
|
const changeTpl = join(vaultPath, 'Templates', 'Change.md');
|
|
489
494
|
if (!existsSync(changeTpl)) {
|
package/src/skills-seed.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/skills-seed.mjs — native, zero-dep process skills (Pilar A: the HOW layer).
|
|
2
2
|
// Seeded into the vault's .brain/skills; distributed by `wendkeep sync-defs` to
|
|
3
|
-
// .claude/skills. Wendkeep-flavored
|
|
3
|
+
// .claude/skills + .agents/skills. Wendkeep-flavored, concise native prose.
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
@@ -41,12 +41,13 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
41
41
|
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
42
42
|
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
43
43
|
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
44
|
-
|
|
44
|
+
quando a change mexe numa capability. Toda autoria de spec ocorre somente em
|
|
45
|
+
\`08-Mudanças/<slug>/specs/<capability>/spec.md\`; \`07-Specs\` é gerado/read-only.
|
|
45
46
|
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
46
47
|
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
47
48
|
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
48
49
|
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
49
|
-
|
|
50
|
+
usa somente os requisitos autocontidos de \`verificacao.json\` e grava \`verdict.json\`. Change trivial (sem
|
|
50
51
|
\`[req:]\`) recebe verdict automático — pula este passe.
|
|
51
52
|
6. **Archive** — \`wendkeep change archive <slug>\`. O *gate* exige sensores verdes **E**
|
|
52
53
|
\`verdict.json\` cobrindo os \`[req:]\`. Passando, promove os deltas pro \`07-Specs\`,
|
|
@@ -55,8 +56,8 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
55
56
|
## Regras
|
|
56
57
|
|
|
57
58
|
- Várias changes podem ficar abertas. \`CURRENT_CHANGE.md\` marca uma atual, sem esconder as
|
|
58
|
-
outras pendências. Claude, Codex ou outro agente
|
|
59
|
-
\`wendkeep change
|
|
59
|
+
outras pendências. Claude, Codex ou outro agente assumem uma change existente com
|
|
60
|
+
\`wendkeep change use <slug>\` ou \`--change <slug>\` quando disponível.
|
|
60
61
|
- Se uma tarefa não precisa de prova automatizada, não declare sensor — o gate só exige
|
|
61
62
|
o que você declarou. Sem \`[sensor:]\`, o archive não trava.
|
|
62
63
|
- A proposta linka a sessão de origem; a sessão linka a mudança ativa. É de propósito:
|
|
@@ -77,7 +78,8 @@ vermelho pedindo por ele — e nunca escreva um teste que passaria sob a impleme
|
|
|
77
78
|
|
|
78
79
|
## Derive do spec, não do código
|
|
79
80
|
|
|
80
|
-
Escreva a asserção a partir do *critério de aceite*
|
|
81
|
+
Escreva a asserção a partir do *critério de aceite* da spec efetiva
|
|
82
|
+
(\`wendkeep spec effective --change <slug>\`), não lendo a
|
|
81
83
|
implementação. Cada asserção mira o resultado que o spec definiu. Escrever o teste lendo o
|
|
82
84
|
código = ele só confirma o que o código já faz, bugs inclusos.
|
|
83
85
|
|
|
@@ -214,13 +216,13 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
|
|
|
214
216
|
## O que fazer
|
|
215
217
|
|
|
216
218
|
1. Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência).
|
|
217
|
-
2. **Re-derive a cobertura do
|
|
218
|
-
|
|
219
|
+
2. **Re-derive a cobertura do pacote autocontido** — pra cada requisito completo em
|
|
220
|
+
\`verificacao.json\`, cheque se o comportamento está coberto por um teste que discrimina (não
|
|
219
221
|
passaria sob impl errada). Evidência \`arquivo:linha\`.
|
|
220
222
|
3. Outcome check ancorado no spec: o resultado observável bate com o critério de aceite?
|
|
221
223
|
4. Grave \`08-Mudanças/<slug>/verdict.json\`:
|
|
222
|
-
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "tasksHash": "<copie do verificacao.json>", "notes": [] }\`.
|
|
223
|
-
|
|
224
|
+
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "tasksHash": "<copie do verificacao.json>", "effectiveSpecHash": "<copie do verificacao.json>", "notes": [] }\`.
|
|
225
|
+
\`tasksHash\` e \`effectiveSpecHash\` vêm do pacote — são selos de frescor; sem eles (ou com tarefas/spec alteradas
|
|
224
226
|
depois), o gate rejeita o verdict como stale.
|
|
225
227
|
|
|
226
228
|
## Regras
|
|
@@ -259,6 +261,7 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
259
261
|
needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
|
|
260
262
|
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
261
263
|
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies).
|
|
264
|
+
Author specs only in \`08-Changes/<slug>/specs/\`; \`07-Specs\` is generated/read-only.
|
|
262
265
|
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
263
266
|
the verification package.
|
|
264
267
|
5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
|
|
@@ -269,7 +272,7 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
269
272
|
## Rules
|
|
270
273
|
- Multiple changes may stay open. \`CURRENT_CHANGE.md\` marks one current change without hiding
|
|
271
274
|
other pending tasks. Any agent may take over an existing change with
|
|
272
|
-
\`wendkeep change
|
|
275
|
+
\`wendkeep change use <slug>\` or \`--change <slug>\` where available.
|
|
273
276
|
- No \`[sensor:]\` on a task = no automated gate for it. No \`[req:]\` = no independent verdict.
|
|
274
277
|
- The graph links session ↔ change ↔ requirement ↔ decision. That is the point.
|
|
275
278
|
`;
|
|
@@ -286,7 +289,7 @@ would pass under the wrong implementation.
|
|
|
286
289
|
4. **Refactor** with the greens protecting you.
|
|
287
290
|
|
|
288
291
|
## Derive from the spec, not the code
|
|
289
|
-
Write assertions from the requirement
|
|
292
|
+
Write assertions from the effective requirement (\`wendkeep spec effective --change <slug>\`), not by reading the
|
|
290
293
|
implementation. Reading the code to write the test = it only confirms what the code already does.
|
|
291
294
|
|
|
292
295
|
## Non-shallow litmus
|
|
@@ -375,17 +378,18 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
|
|
|
375
378
|
|
|
376
379
|
## What to do
|
|
377
380
|
1. Read the package \`08-Changes/<slug>/verificacao.json\` (requirements, tasks, evidence).
|
|
378
|
-
2. **Re-derive coverage from
|
|
381
|
+
2. **Re-derive coverage from the self-contained package** — for each complete requirement in
|
|
382
|
+
\`verificacao.json\`, check its behaviour
|
|
379
383
|
is covered by a test that discriminates (wouldn't pass under a wrong impl). \`file:line\` evidence.
|
|
380
384
|
3. Spec-anchored outcome check: does the observable result match the acceptance criterion?
|
|
381
385
|
4. Write \`08-Changes/<slug>/verdict.json\`:
|
|
382
|
-
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "notes": [] }\`.
|
|
386
|
+
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "file:line" }], "tasksHash": "<copy from the package>", "effectiveSpecHash": "<copy from the package>", "notes": [] }\`.
|
|
383
387
|
|
|
384
388
|
## Rules
|
|
385
389
|
- **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
|
|
386
390
|
- \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
|
|
387
391
|
- Don't fix here — a gap becomes a fix task; re-verify after.
|
|
388
|
-
- The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\`) covering every \`[req:]\`.
|
|
392
|
+
- The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\` and \`effectiveSpecHash\`) covering every \`[req:]\`.
|
|
389
393
|
|
|
390
394
|
## Templates (in this folder)
|
|
391
395
|
- \`spec-reviewer-prompt.md\` — hand it to the verifier sub-agent you spawn (read-only, author≠verifier).
|
|
@@ -402,6 +406,7 @@ const VERDICT_TEMPLATE = `{
|
|
|
402
406
|
{ "req": "GATE-1", "covered": true, "evidence": "tests/foo.test.mjs:42" }
|
|
403
407
|
],
|
|
404
408
|
"tasksHash": "<copie de verificacao.json — selo de frescor / copy from verificacao.json — freshness seal>",
|
|
409
|
+
"effectiveSpecHash": "<copie de verificacao.json / copy from verificacao.json>",
|
|
405
410
|
"notes": []
|
|
406
411
|
}
|
|
407
412
|
`;
|
|
@@ -415,8 +420,8 @@ Claude). Ele NÃO é o autor: entra fresco, read-only, não edita nada.
|
|
|
415
420
|
Você é o verificador independente de uma mudança do wendkeep. Não escreveu este código —
|
|
416
421
|
entre como se nunca o tivesse visto. Read-only.
|
|
417
422
|
|
|
418
|
-
Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos
|
|
419
|
-
|
|
423
|
+
Leia somente o pacote autocontido \`08-Mudanças/<slug>/verificacao.json\` (requisitos completos,
|
|
424
|
+
tarefas e evidência). Não reabra \`07-Specs\`: ele ainda não contém deltas não arquivados.
|
|
420
425
|
|
|
421
426
|
Para cada \`[req:ID]\` da mudança:
|
|
422
427
|
1. Leia o **critério de aceite do requisito** no spec — NÃO leia a implementação primeiro.
|
|
@@ -427,7 +432,7 @@ Para cada \`[req:ID]\` da mudança:
|
|
|
427
432
|
|
|
428
433
|
Grave \`08-Mudanças/<slug>/verdict.json\` no formato de \`verdict-template.json\`. \`ok: false\` se
|
|
429
434
|
qualquer \`[req:]\` não tem cobertura que discrimina. Não conserte aqui — gap vira tarefa de
|
|
430
|
-
correção.
|
|
435
|
+
correção. \`tasksHash\` e \`effectiveSpecHash\` vêm do pacote; alterações posteriores deixam o verdict stale.
|
|
431
436
|
---
|
|
432
437
|
`;
|
|
433
438
|
|
|
@@ -440,8 +445,8 @@ It is NOT the author: fresh context, read-only, edits nothing.
|
|
|
440
445
|
You are the independent verifier of a wendkeep change. You did not write this code — enter as if
|
|
441
446
|
you'd never seen it. Read-only.
|
|
442
447
|
|
|
443
|
-
Read the
|
|
444
|
-
|
|
448
|
+
Read only the self-contained \`08-Changes/<slug>/verificacao.json\` package (complete requirements,
|
|
449
|
+
tasks, evidence). Do not reopen \`07-Specs\`: it does not contain unarchived deltas yet.
|
|
445
450
|
|
|
446
451
|
For each \`[req:ID]\`:
|
|
447
452
|
1. Read the requirement's **acceptance criterion** in the spec — do NOT read the implementation first.
|
|
@@ -452,7 +457,7 @@ For each \`[req:ID]\`:
|
|
|
452
457
|
|
|
453
458
|
Write \`08-Changes/<slug>/verdict.json\` in the shape of \`verdict-template.json\`. \`ok: false\` if any
|
|
454
459
|
\`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`
|
|
455
|
-
|
|
460
|
+
and \`effectiveSpecHash\` come from the package (freshness seals; later task/spec edits make verdict stale).
|
|
456
461
|
---
|
|
457
462
|
`;
|
|
458
463
|
|
package/src/spec.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// `wendkeep spec <sub>` — read-only views over the living specs in 07-Specs (0.7.0).
|
|
2
2
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
adoptSpecsState,
|
|
6
|
+
buildEffectiveRequirementPackage,
|
|
7
|
+
captureSpecBaseline,
|
|
8
|
+
discoverSpecDeltas,
|
|
9
|
+
parseRequirements,
|
|
10
|
+
specConflicts,
|
|
11
|
+
} from '../hooks/spec-core.mjs';
|
|
12
|
+
import { activeChange, parseTasks } from '../hooks/change-core.mjs';
|
|
5
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
6
14
|
|
|
7
15
|
function resolveVault(argv) {
|
|
@@ -24,6 +32,64 @@ export function runSpec(argv) {
|
|
|
24
32
|
const vaultBase = resolveVault(rest);
|
|
25
33
|
const specsDir = join(vaultBase, getLocale(vaultBase).folders.specs);
|
|
26
34
|
|
|
35
|
+
const option = (name) => {
|
|
36
|
+
const index = rest.indexOf(name);
|
|
37
|
+
if (index >= 0) return rest[index + 1];
|
|
38
|
+
const entry = rest.find((a) => a.startsWith(`${name}=`));
|
|
39
|
+
return entry ? entry.slice(name.length + 1) : undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (sub === 'effective') {
|
|
43
|
+
const slug = option('--change') || activeChange(vaultBase);
|
|
44
|
+
if (!slug) { process.stderr.write('wendkeep spec effective: no change (--change or current)\n'); process.exit(2); }
|
|
45
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
46
|
+
let tasks = [];
|
|
47
|
+
try {
|
|
48
|
+
readFileSync(join(changeDir, 'proposta.md'), 'utf8');
|
|
49
|
+
tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
|
|
52
|
+
const reqIds = [...new Set(tasks.map((task) => task.req).filter(Boolean))];
|
|
53
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
54
|
+
if (effective.errors.length) {
|
|
55
|
+
process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (rest.includes('--json')) {
|
|
59
|
+
process.stdout.write(`${JSON.stringify({ slug, effectiveSpecHash: effective.hash, specs: effective.specs }, null, 2)}\n`);
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(`change: ${slug}\neffective-spec-hash: ${effective.hash}\n`);
|
|
62
|
+
for (const spec of effective.specs) {
|
|
63
|
+
process.stdout.write(`spec: ${spec.capability}\n`);
|
|
64
|
+
for (const req of spec.requirements) process.stdout.write(` ${req.operation === 'BASE' ? '=' : req.operation === 'ADDED' ? '+' : '~'} ${req.id || req.name} [${req.source}]\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sub === 'migrate') {
|
|
71
|
+
const state = adoptSpecsState(vaultBase);
|
|
72
|
+
process.stdout.write(`spec state adopted: ${Object.keys(state.specs).length} living spec(s); 07-Specs is generated/read-only\n`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sub === 'rebase') {
|
|
77
|
+
const slug = option('--change') || activeChange(vaultBase);
|
|
78
|
+
if (!slug) { process.stderr.write('wendkeep spec rebase: no change (--change or current)\n'); process.exit(2); }
|
|
79
|
+
const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
80
|
+
try { readFileSync(join(changeDir, 'proposta.md'), 'utf8'); }
|
|
81
|
+
catch { process.stderr.write(`wendkeep spec rebase: change not found: ${slug}\n`); process.exit(2); }
|
|
82
|
+
const capabilities = discoverSpecDeltas(changeDir);
|
|
83
|
+
const conflicts = specConflicts(vaultBase, changeDir, capabilities);
|
|
84
|
+
if (conflicts.length && !rest.includes('--accept-current')) {
|
|
85
|
+
process.stderr.write(`wendkeep spec rebase: conflicts: ${conflicts.join('; ')} — reconcile delta, then rerun with --accept-current\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
captureSpecBaseline(vaultBase, changeDir, { refresh: true });
|
|
89
|
+
process.stdout.write(`spec baseline rebased: ${slug}${conflicts.length ? ` (${conflicts.length} conflict(s) accepted)` : ''}\n`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
27
93
|
if (sub === 'list') {
|
|
28
94
|
let files = [];
|
|
29
95
|
try { files = readdirSync(specsDir).filter((f) => f.endsWith('.md') && f !== 'README.md'); } catch { /* sem specs */ }
|
|
@@ -49,6 +115,6 @@ export function runSpec(argv) {
|
|
|
49
115
|
process.exit(0);
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show.\n`);
|
|
118
|
+
process.stderr.write(`wendkeep spec: unknown subcommand "${sub}". Known: list, show, effective, migrate, rebase.\n`);
|
|
53
119
|
process.exit(2);
|
|
54
120
|
}
|
package/src/sync-defs.mjs
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// they travel with the project in git. They have no automatic consumer — agents
|
|
3
3
|
// read them from their own dirs — so `wendkeep sync-defs` copies them there:
|
|
4
4
|
// .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
|
|
5
|
-
// .brain/skills/<name>/ -> <project>/.claude/skills/
|
|
5
|
+
// .brain/skills/<name>/ -> <project>/.claude/skills/ + .agents/skills/ (skill format)
|
|
6
6
|
// .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
|
|
7
7
|
// cross-platform robustness.
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
10
12
|
import { seedWkSkills } from './skills-seed.mjs';
|
|
11
13
|
import { getLocale } from '../hooks/locale.mjs';
|
|
12
14
|
|
|
@@ -15,6 +17,27 @@ import { getLocale } from '../hooks/locale.mjs';
|
|
|
15
17
|
// the markers is ours; user content around it is never touched.
|
|
16
18
|
const AG_START = '<!-- wendkeep:skills:start -->';
|
|
17
19
|
const AG_END = '<!-- wendkeep:skills:end -->';
|
|
20
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const WENDKEEP_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
22
|
+
const META_FILE = '.wendkeep-meta.json';
|
|
23
|
+
|
|
24
|
+
function directoryHash(root) {
|
|
25
|
+
const hash = createHash('sha256');
|
|
26
|
+
const visit = (dir) => {
|
|
27
|
+
let names = [];
|
|
28
|
+
try { names = readdirSync(dir).sort(); } catch { return; }
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
if (name === META_FILE) continue;
|
|
31
|
+
const path = join(dir, name);
|
|
32
|
+
const rel = relative(root, path).replaceAll('\\', '/');
|
|
33
|
+
const stat = statSync(path);
|
|
34
|
+
if (stat.isDirectory()) visit(path);
|
|
35
|
+
else { hash.update(rel); hash.update('\0'); hash.update(readFileSync(path)); hash.update('\0'); }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
visit(root);
|
|
39
|
+
return hash.digest('hex');
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
function skillInventory(skillsSrc) {
|
|
20
43
|
const out = [];
|
|
@@ -31,9 +54,10 @@ function skillInventory(skillsSrc) {
|
|
|
31
54
|
return out;
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
function renderAgentsSection(skills) {
|
|
57
|
+
function renderAgentsSection(skills, sourceHash = '') {
|
|
35
58
|
const list = skills.map((s) => `- **${s.name}** — ${s.description}`).join('\n');
|
|
36
59
|
return `${AG_START}
|
|
60
|
+
<!-- wendkeep-version: ${WENDKEEP_VERSION}; skills-sha256: ${sourceHash} -->
|
|
37
61
|
## wendkeep — process skills & loop
|
|
38
62
|
|
|
39
63
|
This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harness. Work
|
|
@@ -41,9 +65,10 @@ through its change loop: \`wendkeep change new <slug>\` → implement tasks test
|
|
|
41
65
|
(tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
|
|
42
66
|
\`wendkeep verify --deep\` + an independent read-only verification pass writing
|
|
43
67
|
\`verdict.json\` → \`wendkeep change archive\` (gated). Inspect with \`wendkeep change
|
|
44
|
-
status\` / \`spec
|
|
68
|
+
status\` / \`spec effective --change <slug>\` / \`sensors list\`. Author specs only in
|
|
69
|
+
\`08-Mudanças/<slug>/specs/\`; \`07-Specs\` is generated and must not be edited directly.
|
|
45
70
|
|
|
46
|
-
Process skills (full text in \`.claude/skills
|
|
71
|
+
Process skills (full text in \`.claude/skills/\`, \`.agents/skills/\`, and the vault's \`.brain/skills/\`):
|
|
47
72
|
${list}
|
|
48
73
|
${AG_END}`;
|
|
49
74
|
}
|
|
@@ -52,7 +77,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
|
|
|
52
77
|
const skills = skillInventory(skillsSrc);
|
|
53
78
|
if (!skills.length) return false;
|
|
54
79
|
const path = join(projectPath, 'AGENTS.md');
|
|
55
|
-
const section = renderAgentsSection(skills);
|
|
80
|
+
const section = renderAgentsSection(skills, directoryHash(skillsSrc));
|
|
56
81
|
let content = '';
|
|
57
82
|
try { content = readFileSync(path, 'utf8'); } catch { /* novo */ }
|
|
58
83
|
if (content.includes(AG_START) && content.includes(AG_END)) {
|
|
@@ -67,7 +92,7 @@ function upsertAgentsMd(projectPath, skillsSrc) {
|
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
export function syncDefs(vaultBase, projectPath) {
|
|
70
|
-
const out = { agents: [], skills: [], agentsMd: false };
|
|
95
|
+
const out = { agents: [], skills: [], codexSkills: [], agentsMd: false };
|
|
71
96
|
|
|
72
97
|
const agentsSrc = join(vaultBase, '.brain', 'agents');
|
|
73
98
|
if (existsSync(agentsSrc)) {
|
|
@@ -85,10 +110,19 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
85
110
|
for (const name of readdirSync(skillsSrc)) {
|
|
86
111
|
const dir = join(skillsSrc, name);
|
|
87
112
|
if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
const destinations = [
|
|
114
|
+
join(projectPath, '.claude', 'skills', name),
|
|
115
|
+
join(projectPath, '.agents', 'skills', name),
|
|
116
|
+
];
|
|
117
|
+
const sourceHash = directoryHash(dir);
|
|
118
|
+
for (const dest of destinations) {
|
|
119
|
+
rmSync(dest, { recursive: true, force: true });
|
|
120
|
+
mkdirSync(dest, { recursive: true });
|
|
121
|
+
cpSync(dir, dest, { recursive: true });
|
|
122
|
+
writeFileSync(join(dest, META_FILE), `${JSON.stringify({ wendkeepVersion: WENDKEEP_VERSION, sourceHash }, null, 2)}\n`, 'utf8');
|
|
123
|
+
}
|
|
91
124
|
out.skills.push(name);
|
|
125
|
+
out.codexSkills.push(name);
|
|
92
126
|
}
|
|
93
127
|
}
|
|
94
128
|
|
|
@@ -98,6 +132,33 @@ export function syncDefs(vaultBase, projectPath) {
|
|
|
98
132
|
return out;
|
|
99
133
|
}
|
|
100
134
|
|
|
135
|
+
export function checkSyncDefs(vaultBase, projectPath) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
const skillsSrc = join(vaultBase, '.brain', 'skills');
|
|
138
|
+
if (!existsSync(skillsSrc)) return { ok: false, issues: ['fonte .brain/skills ausente'] };
|
|
139
|
+
const names = readdirSync(skillsSrc).filter((name) => {
|
|
140
|
+
try { return statSync(join(skillsSrc, name)).isDirectory(); } catch { return false; }
|
|
141
|
+
});
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
const expected = directoryHash(join(skillsSrc, name));
|
|
144
|
+
for (const relDest of [join('.claude', 'skills', name), join('.agents', 'skills', name)]) {
|
|
145
|
+
const dest = join(projectPath, relDest);
|
|
146
|
+
if (!existsSync(join(dest, 'SKILL.md'))) { issues.push(`${relDest}: ausente`); continue; }
|
|
147
|
+
if (directoryHash(dest) !== expected) issues.push(`${relDest}: conteúdo divergiu da fonte`);
|
|
148
|
+
let meta = null;
|
|
149
|
+
try { meta = JSON.parse(readFileSync(join(dest, META_FILE), 'utf8')); } catch { /* missing */ }
|
|
150
|
+
if (meta?.wendkeepVersion !== WENDKEEP_VERSION || meta?.sourceHash !== expected) {
|
|
151
|
+
issues.push(`${relDest}: metadata stale/ausente (esperado WendKeep ${WENDKEEP_VERSION})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const expectedSection = renderAgentsSection(skillInventory(skillsSrc), directoryHash(skillsSrc));
|
|
156
|
+
let agentsMd = '';
|
|
157
|
+
try { agentsMd = readFileSync(join(projectPath, 'AGENTS.md'), 'utf8'); } catch { /* missing */ }
|
|
158
|
+
if (!agentsMd.includes(expectedSection)) issues.push('AGENTS.md: bloco gerenciado stale/ausente');
|
|
159
|
+
return { ok: issues.length === 0, issues, version: WENDKEEP_VERSION, skills: names.length };
|
|
160
|
+
}
|
|
161
|
+
|
|
101
162
|
// CLI entry for `wendkeep sync-defs`.
|
|
102
163
|
export function runSyncDefs(argv) {
|
|
103
164
|
let vault;
|
|
@@ -116,6 +177,15 @@ export function runSyncDefs(argv) {
|
|
|
116
177
|
}
|
|
117
178
|
const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
118
179
|
const projectPath = resolve(project || process.cwd());
|
|
180
|
+
if (argv.includes('--check')) {
|
|
181
|
+
const r = checkSyncDefs(vaultBase, projectPath);
|
|
182
|
+
if (r.ok) process.stdout.write(`wendkeep sync-defs --check: ok (${r.skills} skill(s), ${r.version})\n`);
|
|
183
|
+
else {
|
|
184
|
+
process.stderr.write(`wendkeep sync-defs --check: drift detectado\n - ${r.issues.join('\n - ')}\n`);
|
|
185
|
+
process.stderr.write('rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex\n');
|
|
186
|
+
}
|
|
187
|
+
process.exit(r.ok ? 0 : 1);
|
|
188
|
+
}
|
|
119
189
|
// --reseed (0.31.0): sobrescreve as wk-* de .brain/skills com os seeds da versão instalada
|
|
120
190
|
// ANTES de copiar — é como um vault existente recebe descriptions/HARD-GATE novos.
|
|
121
191
|
if (argv.includes('--reseed')) {
|
|
@@ -124,7 +194,7 @@ export function runSyncDefs(argv) {
|
|
|
124
194
|
}
|
|
125
195
|
const r = syncDefs(vaultBase, projectPath);
|
|
126
196
|
process.stdout.write(
|
|
127
|
-
`wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
|
|
197
|
+
`wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills + .agents/skills\n`,
|
|
128
198
|
);
|
|
129
199
|
if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
|
|
130
200
|
if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
|
|
@@ -151,8 +221,8 @@ model = "gpt-5.5"
|
|
|
151
221
|
const SKILLS_README = `# .brain/skills — versioned custom skill definitions
|
|
152
222
|
|
|
153
223
|
Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
|
|
154
|
-
\`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\`
|
|
155
|
-
|
|
224
|
+
\`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` and
|
|
225
|
+
\`<project>/.agents/skills/\`. Edit here (source of truth); re-run sync after changes.
|
|
156
226
|
`;
|
|
157
227
|
|
|
158
228
|
const EXAMPLE_SKILL = `---
|
package/src/verify.mjs
CHANGED
|
@@ -5,7 +5,11 @@ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildEffectiveRequirementPackage,
|
|
10
|
+
captureSpecBaseline,
|
|
11
|
+
tasksHashOf,
|
|
12
|
+
} from '../hooks/spec-core.mjs';
|
|
9
13
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
10
14
|
import { getLocale } from '../hooks/locale.mjs';
|
|
11
15
|
|
|
@@ -88,16 +92,36 @@ export function runVerify(argv) {
|
|
|
88
92
|
const tasks = parseTasks(tarefas);
|
|
89
93
|
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
90
94
|
const tasksHash = tasksHashOf(tarefas);
|
|
95
|
+
captureSpecBaseline(vaultBase, changeDir);
|
|
96
|
+
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
97
|
+
if (effective.errors.length) {
|
|
98
|
+
process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (effective.missing.length) {
|
|
102
|
+
process.stderr.write(`verify --deep: requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}\n`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
91
105
|
const pkg = {
|
|
92
106
|
slug,
|
|
93
107
|
tasksHash,
|
|
94
|
-
|
|
108
|
+
effectiveSpecHash: effective.hash,
|
|
109
|
+
requirements: effective.requirements.map((req) => {
|
|
110
|
+
return {
|
|
111
|
+
id: req.id,
|
|
112
|
+
name: req.name,
|
|
113
|
+
capability: req.capability,
|
|
114
|
+
operation: req.operation,
|
|
115
|
+
source: req.source,
|
|
116
|
+
body: req.body,
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
95
119
|
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
96
120
|
sensors: evidence,
|
|
97
121
|
};
|
|
98
122
|
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
99
123
|
if (reqIds.length === 0) {
|
|
100
|
-
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
124
|
+
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
101
125
|
process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
|
|
102
126
|
} else {
|
|
103
127
|
process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');
|