wendkeep 0.4.0 → 0.6.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 +129 -0
- package/LICENSE +21 -0
- package/bin/wendkeep.mjs +6 -0
- package/hooks/brain-inject.mjs +3 -1
- package/hooks/change-core.mjs +44 -10
- package/hooks/harness-doctor.mjs +77 -0
- package/hooks/lessons-core.mjs +43 -0
- package/hooks/sensors-core.mjs +23 -1
- package/hooks/spec-core.mjs +33 -16
- package/package.json +12 -3
- package/src/change.mjs +15 -5
- package/src/doctor.mjs +22 -10
- package/src/lessons.mjs +35 -0
- package/src/skills-seed.mjs +85 -22
- package/src/taxonomy.mjs +2 -0
- package/src/verify.mjs +38 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **wendkeep** are documented here. Format based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.6.0] — 2026-07-05
|
|
8
|
+
|
|
9
|
+
Enforcement layer (Wave B of the TLC-parity program) — closes TLC parity.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Discrimination sensor (`type: mutation`):** delegates to the project's mutation tool and
|
|
13
|
+
parses its mutation-testing-elements report; surviving mutants become fix tasks in the active
|
|
14
|
+
change (`- [ ] M.n mata mutante file:line`), bounded to 3 rounds before escalating.
|
|
15
|
+
- **Harness self-check:** `wendkeep doctor` now validates the a2 state — an invalid
|
|
16
|
+
`wendkeep.sensors.json`, a broken `CURRENT_CHANGE` pointer, changes without a `proposta.md`,
|
|
17
|
+
an orphan `[req:]` (unknown requirement), and stale verdicts.
|
|
18
|
+
- **Lessons loop:** `wendkeep lesson add "<trigger>" "<lesson>"` records a project-local lesson in
|
|
19
|
+
`.brain/lessons/`; `brain-inject` surfaces the recent ones as a `<lessons>` block at SessionStart.
|
|
20
|
+
- **Auto-sizing:** `wendkeep change new <slug> --simple` scaffolds only proposta + tarefas
|
|
21
|
+
(no design / spec-delta) for trivial changes.
|
|
22
|
+
- **Harness contract v1.1** (`docs/14-harness-contract.md`): the mutation + lesson formats.
|
|
23
|
+
|
|
24
|
+
## [0.5.0] — 2026-07-05
|
|
25
|
+
|
|
26
|
+
Verification & credibility layer (Wave A of the TLC-parity program). The gate stops being
|
|
27
|
+
"green sensors" alone and starts requiring an independent verdict for changes that touch a spec.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Requirement IDs + traceability:** living-spec requirements carry a stable ID
|
|
31
|
+
(`### Requisito: GATE-1 — nome`); tasks reference them with `[req:<ID>]`; the archive ADR
|
|
32
|
+
lists the requirements it satisfied. Rastro req → task → verdict → ADR.
|
|
33
|
+
- **`wendkeep verify --deep`:** assembles a verification package (`verificacao.json`) for an
|
|
34
|
+
independent pass. A trivial change (no `[req:]`, sensors green) gets an auto verdict.
|
|
35
|
+
- **Independent verdict gate:** `change archive` now also requires `verdict.json` (`ok`, covering
|
|
36
|
+
every declared `[req:]`) for requirement-bearing changes.
|
|
37
|
+
- **TLC-grade process skills:** rewrote `wk-tdd` (spec-derived assertions, non-shallow litmus,
|
|
38
|
+
test adequacy, test-learning) and `wk-brainstorming` (closure gate + out-of-scope); new
|
|
39
|
+
`wk-verify` (fresh read-only verifier, author≠verifier).
|
|
40
|
+
- **Harness contract v1** (`docs/14-harness-contract.md`): the extension-point formats.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Requirement-less changes are unaffected — the sensor gate remains their proof. The verdict
|
|
44
|
+
requirement applies only when a change declares `[req:]` tasks. Specs from 0.4.0 (headings
|
|
45
|
+
without an ID) stay valid.
|
|
46
|
+
|
|
47
|
+
## [0.4.0] — 2026-07-05
|
|
48
|
+
|
|
49
|
+
Spec promotion (the living contract) + harness fixes.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- **Spec promotion** — `wendkeep change archive` merges each capability's spec delta
|
|
53
|
+
(`## ADDED` / `## MODIFIED` / `## REMOVED Requirements`) into the living
|
|
54
|
+
`07-Specs/<capability>.md`. Multi-capability per change via `specs: [slugs]` in the
|
|
55
|
+
proposta; the living spec footer wikilinks the archived change (`hooks/spec-core.mjs`).
|
|
56
|
+
- Change scaffold now seeds an example spec delta at `specs/exemplo/spec.md`.
|
|
57
|
+
- `wendkeep change archive` prints promoted capabilities and surfaces delta warnings
|
|
58
|
+
(ADDED-already-exists / MODIFIED-missing) without blocking.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- `wendkeep init` now runs `sync-defs` itself — process skills and agents are delivered
|
|
62
|
+
to `.claude/skills` / `.codex/agents` immediately (no manual step).
|
|
63
|
+
- The archive gate and `wendkeep verify` share one rule: only **critical** (or missing)
|
|
64
|
+
sensors block; a red `warning` sensor is advisory. Evidence records each sensor's severity.
|
|
65
|
+
- README documents the change/verify/skills commands and the a2 loop.
|
|
66
|
+
|
|
67
|
+
## [0.3.0] — 2026-07-05
|
|
68
|
+
|
|
69
|
+
The a2 native harness — a zero-dependency spec→change→proof loop on the vault memory
|
|
70
|
+
core (recreates the best of OpenSpec + dotcontext + superpowers, natively).
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- **Pilar B — change lifecycle:** `wendkeep change new|list|show|archive`. Scaffolds
|
|
74
|
+
`08-Mudanças/<slug>/` (proposta/design/tarefas); the active change is injected at the
|
|
75
|
+
next `SessionStart`; archive moves the change to `_arquivo/` and mints an ADR in
|
|
76
|
+
`04-Decisões/`. New vault folders `07-Specs/`, `08-Mudanças/` (`hooks/change-core.mjs`).
|
|
77
|
+
- **Pilar C — verify + gate:** `wendkeep verify` runs a change's task-declared sensors
|
|
78
|
+
(`[sensor:<id>]` hints) from a native `wendkeep.sensors.json`, records `evidencia.json`;
|
|
79
|
+
`change archive` gates on the evidence (`hooks/sensors-core.mjs`).
|
|
80
|
+
- **Pilar A — process skills:** native `wk-workflow` / `wk-tdd` / `wk-debugging` /
|
|
81
|
+
`wk-brainstorming` / `wk-planning` seeded into `.brain/skills` (`src/skills-seed.mjs`).
|
|
82
|
+
|
|
83
|
+
## [0.2.7] — 2026-06-30
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
- **Definitions layer:** `.brain/agents/` + `.brain/skills/` as versioned source of truth,
|
|
87
|
+
copied into the project with `wendkeep sync-defs`.
|
|
88
|
+
- **dotcontext seed:** a starter `.context/config/sensors.json` (a `validate-memory` sensor
|
|
89
|
+
plus one per detected `package.json` script) when the dotcontext companion is selected.
|
|
90
|
+
|
|
91
|
+
## [0.2.1] – [0.2.6] — 2026-06-29
|
|
92
|
+
|
|
93
|
+
Rapid iteration on the companion + memory layers (same day):
|
|
94
|
+
- **Companions** wired the most agent-agnostic way (context-mode / dotcontext as MCP,
|
|
95
|
+
understand-anything via a domain-graph SessionStart injector, caveman via installer).
|
|
96
|
+
- **Obsidian color system** — a mode-agnostic CSS snippet (note-type accents) + graph
|
|
97
|
+
color groups, merged non-destructively into `.obsidian/`.
|
|
98
|
+
- **Curated memory protocol** — `.brain/CORE.md` + `COMPACTION_PROTOCOL.md` and
|
|
99
|
+
`wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
100
|
+
- Cross-platform caveman installer fix (npx non-interactive; Gemini excluded).
|
|
101
|
+
- Derived notes grouped by month under the year.
|
|
102
|
+
|
|
103
|
+
## [0.2.0] — 2026-06-29
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
- Companion plugins/MCP selection in `wendkeep init` (context-mode, understand-anything,
|
|
107
|
+
caveman) with idempotent settings/`.mcp.json` merging.
|
|
108
|
+
|
|
109
|
+
## [0.1.0] — 2026-06-29
|
|
110
|
+
|
|
111
|
+
Initial release — the capture engine, extracted from a system in daily production use.
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
- Automatic session capture (`SessionStart` / `UserPromptSubmit` / `Stop` hooks) into
|
|
115
|
+
`02-Sessões/` as turn-by-turn Markdown.
|
|
116
|
+
- Multi-agent provider detection (Claude Code, Codex, Copilot).
|
|
117
|
+
- Token & cost tracking (cache-aware `pricing.json`).
|
|
118
|
+
- Auto-extracted derived notes (decisions / bugs / learnings), backlinked to the session.
|
|
119
|
+
- Curated memory (`.brain/` cold index + `CORE` + `DIGEST` injected at `SessionStart`).
|
|
120
|
+
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
121
|
+
|
|
122
|
+
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
123
|
+
[0.6.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.6.0
|
|
124
|
+
[0.5.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.5.0
|
|
125
|
+
[0.4.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.4.0
|
|
126
|
+
[0.3.0]: https://www.npmjs.com/package/wendkeep/v/0.3.0
|
|
127
|
+
[0.2.7]: https://www.npmjs.com/package/wendkeep/v/0.2.7
|
|
128
|
+
[0.2.0]: https://www.npmjs.com/package/wendkeep/v/0.2.0
|
|
129
|
+
[0.1.0]: https://www.npmjs.com/package/wendkeep/v/0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roger Alves
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -45,6 +45,7 @@ Usage:
|
|
|
45
45
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
46
46
|
wendkeep change <sub> Change lifecycle: new <slug> | list | show <slug> | archive <slug>.
|
|
47
47
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
48
|
+
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
48
49
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
49
50
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
50
51
|
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
@@ -111,6 +112,11 @@ async function main() {
|
|
|
111
112
|
runVerify(rest);
|
|
112
113
|
break;
|
|
113
114
|
}
|
|
115
|
+
case 'lesson': {
|
|
116
|
+
const { runLesson } = await import('../src/lessons.mjs');
|
|
117
|
+
runLesson(rest);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
114
120
|
case '--version':
|
|
115
121
|
case '-v':
|
|
116
122
|
process.stdout.write(`${version()}\n`);
|
package/hooks/brain-inject.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
8
8
|
import { getVaultBase, readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
9
9
|
import { brainDir } from './brain-core.mjs';
|
|
10
10
|
import { buildActiveChangeInjection } from './change-core.mjs';
|
|
11
|
+
import { buildLessonsInjection } from './lessons-core.mjs';
|
|
11
12
|
|
|
12
13
|
const MAX_LINES = 45; // CORE ≤25 + DIGEST ≤15 + folga; salvaguarda se o CORE crescer à mão
|
|
13
14
|
|
|
@@ -26,7 +27,8 @@ export function buildInjection(vaultBase) {
|
|
|
26
27
|
}
|
|
27
28
|
const brain = ['<brain_memory>', ...lines, pointer, '</brain_memory>'].join('\n');
|
|
28
29
|
const change = buildActiveChangeInjection(vaultBase);
|
|
29
|
-
|
|
30
|
+
const lessons = buildLessonsInjection(vaultBase);
|
|
31
|
+
return [brain, change, lessons].filter(Boolean).join('\n');
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
package/hooks/change-core.mjs
CHANGED
|
@@ -79,7 +79,7 @@ export function clearActiveChange(vaultBase) {
|
|
|
79
79
|
if (existsSync(p)) writeFileSync(p, 'change:\n', 'utf8');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function newChange(vaultBase, slug, { sessionRel = '', dateStr }) {
|
|
82
|
+
export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple = false }) {
|
|
83
83
|
const dir = join(vaultBase, CHANGES_DIR, slug);
|
|
84
84
|
const existed = existsSync(join(dir, 'proposta.md'));
|
|
85
85
|
mkdirSync(dir, { recursive: true });
|
|
@@ -89,13 +89,15 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr }) {
|
|
|
89
89
|
if (!existsSync(f)) writeFileSync(f, content, 'utf8');
|
|
90
90
|
};
|
|
91
91
|
write('proposta.md', files.proposta);
|
|
92
|
-
write('design.md', files.design);
|
|
93
92
|
write('tarefas.md', files.tarefas);
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
// Auto-sizing (Wave B): a --simple change skips the design + spec-delta scaffold.
|
|
94
|
+
if (!simple) {
|
|
95
|
+
write('design.md', files.design);
|
|
96
|
+
const exampleDelta = join(dir, 'specs', 'exemplo', 'spec.md');
|
|
97
|
+
if (!existsSync(exampleDelta)) {
|
|
98
|
+
mkdirSync(join(dir, 'specs', 'exemplo'), { recursive: true });
|
|
99
|
+
writeFileSync(exampleDelta, files.specDelta, 'utf8');
|
|
100
|
+
}
|
|
99
101
|
}
|
|
100
102
|
setActiveChange(vaultBase, slug);
|
|
101
103
|
return { rel: changeDirRel(slug), created: !existed };
|
|
@@ -105,13 +107,18 @@ export function parseTasks(md) {
|
|
|
105
107
|
const tasks = [];
|
|
106
108
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
107
109
|
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
110
|
+
const reqRe = /\[req:\s*([A-Z][A-Z0-9]*-\d+)\]/;
|
|
108
111
|
let m;
|
|
109
112
|
while ((m = re.exec(String(md))) !== null) {
|
|
110
113
|
let text = m[3].trim();
|
|
111
114
|
const sm = text.match(sensorRe);
|
|
115
|
+
const rm = text.match(reqRe);
|
|
112
116
|
const sensor = sm ? sm[1] : undefined;
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
const req = rm ? rm[1] : undefined;
|
|
118
|
+
if (sm) text = text.replace(sensorRe, '');
|
|
119
|
+
if (rm) text = text.replace(reqRe, '');
|
|
120
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
121
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(req ? { req } : {}) });
|
|
115
122
|
}
|
|
116
123
|
return tasks;
|
|
117
124
|
}
|
|
@@ -151,6 +158,29 @@ export function activeChangeLink(vaultBase) {
|
|
|
151
158
|
return slug ? `Change ativa: [[${CHANGES_DIR}/${slug}/proposta]]` : '';
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// Append fix tasks for surviving mutants to a change's tarefas.md (Wave B). Deduped by
|
|
162
|
+
// file:line, numbered M.<n> continuing from any existing fix tasks. Returns count added.
|
|
163
|
+
export function appendFixTasks(changeDir, mutants, sensorId) {
|
|
164
|
+
const path = join(changeDir, 'tarefas.md');
|
|
165
|
+
let md = '';
|
|
166
|
+
try { md = readFileSync(path, 'utf8'); } catch { /* nova */ }
|
|
167
|
+
const existing = new Set([...md.matchAll(/mata mutante (\S+):(\d+)/g)].map((m) => `${m[1]}:${m[2]}`));
|
|
168
|
+
const nums = [...md.matchAll(/^-\s+\[[ x]\]\s+M\.(\d+)\b/gm)].map((m) => Number(m[1]));
|
|
169
|
+
let n = nums.length ? Math.max(...nums) : 0;
|
|
170
|
+
const lines = [];
|
|
171
|
+
for (const mut of mutants || []) {
|
|
172
|
+
const key = `${mut.file}:${mut.line}`;
|
|
173
|
+
if (existing.has(key)) continue;
|
|
174
|
+
existing.add(key);
|
|
175
|
+
n += 1;
|
|
176
|
+
lines.push(`- [ ] M.${n} mata mutante ${mut.file}:${mut.line} (${mut.mutator}) [sensor:${sensorId}]`);
|
|
177
|
+
}
|
|
178
|
+
if (!lines.length) return 0;
|
|
179
|
+
const sep = md === '' || md.endsWith('\n') ? '' : '\n';
|
|
180
|
+
writeFileSync(path, `${md}${sep}${lines.join('\n')}\n`, 'utf8');
|
|
181
|
+
return lines.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
// Gate seam (Pilar B stub; Pilar C replaces with real sensor evidence).
|
|
155
185
|
export function gateGreen() {
|
|
156
186
|
return { ok: true, failing: [] };
|
|
@@ -176,6 +206,9 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
176
206
|
}
|
|
177
207
|
} catch { /* proposta ilegível — segue só com ADR */ }
|
|
178
208
|
|
|
209
|
+
let reqIds = [];
|
|
210
|
+
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
|
|
211
|
+
|
|
179
212
|
ensureDir(join(vaultBase, CHANGES_DIR, ARCHIVE_DIR));
|
|
180
213
|
renameSync(src, join(vaultBase, destRel));
|
|
181
214
|
|
|
@@ -187,6 +220,7 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
187
220
|
const capLine = promoted.length
|
|
188
221
|
? `\n\nCapabilities: ${promoted.map((c) => wikilinkFromRel(join(SPECS_DIR, c))).join(', ')}.`
|
|
189
222
|
: '';
|
|
223
|
+
const reqLine = reqIds.length ? `\n\nRequisitos: ${reqIds.join(', ')}.` : '';
|
|
190
224
|
writeFileSync(join(vaultBase, adrRel), `---
|
|
191
225
|
type: decision
|
|
192
226
|
status: accepted
|
|
@@ -201,7 +235,7 @@ tags:
|
|
|
201
235
|
|
|
202
236
|
## Decisão
|
|
203
237
|
|
|
204
|
-
Mudança ${changeWikilink} concluída e arquivada.${capLine}
|
|
238
|
+
Mudança ${changeWikilink} concluída e arquivada.${capLine}${reqLine}
|
|
205
239
|
`, 'utf8');
|
|
206
240
|
|
|
207
241
|
clearActiveChange(vaultBase);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// hooks/harness-doctor.mjs — integrity checks for the a2 harness state (Wave B).
|
|
2
|
+
// Pure-ish (fs reads only). `wendkeep doctor` reports errors (exit 1) + warnings.
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { activeChange, parseTasks } from './change-core.mjs';
|
|
6
|
+
import { parseRequirements, parseSpecsList, parseDelta, evaluateVerdict } from './spec-core.mjs';
|
|
7
|
+
|
|
8
|
+
const CHANGES_DIR = '08-Mudanças';
|
|
9
|
+
const SPECS_DIR = '07-Specs';
|
|
10
|
+
|
|
11
|
+
export function checkHarness(vaultBase, projectRoot) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
|
|
15
|
+
// 1. wendkeep.sensors.json well-formed.
|
|
16
|
+
const sensorsPath = join(projectRoot, 'wendkeep.sensors.json');
|
|
17
|
+
if (existsSync(sensorsPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(readFileSync(sensorsPath, 'utf8'));
|
|
20
|
+
if (!Array.isArray(data.sensors)) errors.push('wendkeep.sensors.json: "sensors" não é lista');
|
|
21
|
+
else for (const s of data.sensors) if (!s.id || !s.command) errors.push(`sensor sem id/command: ${JSON.stringify(s.id || '?')}`);
|
|
22
|
+
} catch { errors.push('wendkeep.sensors.json: JSON inválido'); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Known requirement ids across the living specs (+ spec-without-source warning).
|
|
26
|
+
const knownReqs = new Set();
|
|
27
|
+
try {
|
|
28
|
+
for (const f of readdirSync(join(vaultBase, SPECS_DIR))) {
|
|
29
|
+
if (!f.endsWith('.md') || f === 'README.md') continue;
|
|
30
|
+
const md = readFileSync(join(vaultBase, SPECS_DIR, f), 'utf8');
|
|
31
|
+
for (const r of parseRequirements(md)) if (r.id) knownReqs.add(r.id);
|
|
32
|
+
if (!/^>\s+Atualizado por/m.test(md)) warnings.push(`spec sem origem: ${SPECS_DIR}/${f}`);
|
|
33
|
+
}
|
|
34
|
+
} catch { /* sem 07-Specs */ }
|
|
35
|
+
|
|
36
|
+
// 2/3. Changes: malformed dirs; the active change's deltas add to knownReqs.
|
|
37
|
+
const active = activeChange(vaultBase);
|
|
38
|
+
let names = [];
|
|
39
|
+
try { names = readdirSync(join(vaultBase, CHANGES_DIR)).filter((n) => n !== '_arquivo'); } catch { /* none */ }
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const dir = join(vaultBase, CHANGES_DIR, name);
|
|
42
|
+
let entries;
|
|
43
|
+
try { entries = readdirSync(dir); } catch { continue; } // a file, not a change dir
|
|
44
|
+
if (!entries.includes('proposta.md')) { errors.push(`change sem proposta.md: ${name}`); continue; }
|
|
45
|
+
if (name === active) {
|
|
46
|
+
try {
|
|
47
|
+
for (const cap of parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8'))) {
|
|
48
|
+
try {
|
|
49
|
+
const d = parseDelta(readFileSync(join(dir, 'specs', cap, 'spec.md'), 'utf8'));
|
|
50
|
+
for (const r of [...d.added, ...d.modified]) if (r.id) knownReqs.add(r.id);
|
|
51
|
+
} catch { /* sem delta pra essa cap */ }
|
|
52
|
+
}
|
|
53
|
+
} catch { /* proposta ilegível */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4/5. Active-change pointer resolves; [req:] orphans; stale verdict.
|
|
58
|
+
if (active) {
|
|
59
|
+
const dir = join(vaultBase, CHANGES_DIR, active);
|
|
60
|
+
if (!existsSync(join(dir, 'proposta.md'))) {
|
|
61
|
+
errors.push(`ponteiro CURRENT_CHANGE aponta pra change inexistente: ${active}`);
|
|
62
|
+
} else {
|
|
63
|
+
let tasks = [];
|
|
64
|
+
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* sem tarefas */ }
|
|
65
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
66
|
+
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`);
|
|
67
|
+
let verdict = null;
|
|
68
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
69
|
+
if (verdict && reqIds.length) {
|
|
70
|
+
const v = evaluateVerdict(verdict, reqIds);
|
|
71
|
+
if (!v.ok) warnings.push(`verdict stale em ${active}: falta cobrir ${v.missing.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { errors, warnings };
|
|
77
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// hooks/lessons-core.mjs — project-local lessons from verification failures (Wave B).
|
|
2
|
+
// A failure distilled into a terse lesson; brain-inject surfaces the recent ones at
|
|
3
|
+
// SessionStart so the framework gets sharper on your codebase. No external deps.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
function slugify(s) {
|
|
8
|
+
return String(s)
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '')
|
|
13
|
+
.slice(0, 50) || 'licao';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function addLesson(vaultBase, { trigger, lesson, sourceChange = '', dateStr = '' }) {
|
|
17
|
+
const dir = join(vaultBase, '.brain', 'lessons');
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
const path = join(dir, `${dateStr ? `${dateStr}-` : ''}${slugify(trigger)}.md`);
|
|
20
|
+
writeFileSync(
|
|
21
|
+
path,
|
|
22
|
+
`---\ntype: lesson\ntrigger: ${JSON.stringify(String(trigger))}\nsource: ${sourceChange}\ndate: ${dateStr}\n---\n\n${lesson}\n`,
|
|
23
|
+
'utf8',
|
|
24
|
+
);
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Compact <lessons> block of the most recent lessons (filename-sorted desc; a date prefix
|
|
29
|
+
// makes that chronological). Budget-capped by `max`. '' when there are none.
|
|
30
|
+
export function buildLessonsInjection(vaultBase, { max = 5 } = {}) {
|
|
31
|
+
const dir = join(vaultBase, '.brain', 'lessons');
|
|
32
|
+
let files;
|
|
33
|
+
try { files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort().reverse().slice(0, max); }
|
|
34
|
+
catch { return ''; }
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const f of files) {
|
|
37
|
+
try {
|
|
38
|
+
const body = readFileSync(join(dir, f), 'utf8').replace(/^---[\s\S]*?---\n/, '').trim().split('\n')[0];
|
|
39
|
+
if (body) lines.push(`- ${body}`);
|
|
40
|
+
} catch { /* skip */ }
|
|
41
|
+
}
|
|
42
|
+
return lines.length ? `<lessons>\nLições do projeto (de falhas anteriores):\n${lines.join('\n')}\n</lessons>` : '';
|
|
43
|
+
}
|
package/hooks/sensors-core.mjs
CHANGED
|
@@ -26,11 +26,33 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, now } = {}) {
|
|
|
26
26
|
const s = byId[id];
|
|
27
27
|
if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
|
|
28
28
|
const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore' });
|
|
29
|
-
|
|
29
|
+
const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
|
|
30
|
+
if (s.type === 'mutation' && s.report) {
|
|
31
|
+
// Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
|
|
32
|
+
// attach surviving mutants so verify can turn them into fix tasks.
|
|
33
|
+
try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
|
|
34
|
+
catch { /* report ausente/ilegível — segue só com o exit code */ }
|
|
35
|
+
}
|
|
36
|
+
evidence.push(entry);
|
|
30
37
|
}
|
|
31
38
|
return evidence;
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
// Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
|
|
42
|
+
// (Survived | NoCoverage) as {file, line, mutator}.
|
|
43
|
+
export function parseMutationReport(json) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const files = json && json.files ? json.files : {};
|
|
46
|
+
for (const [file, data] of Object.entries(files)) {
|
|
47
|
+
for (const m of (data && data.mutants) || []) {
|
|
48
|
+
if (m.status === 'Survived' || m.status === 'NoCoverage') {
|
|
49
|
+
out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
// A required sensor blocks the gate when it is missing (never verified) or red at a
|
|
35
57
|
// non-warning severity. Warnings are advisory: a red warning does not block archive.
|
|
36
58
|
// Severity comes from the evidence entry (written by runSensors); absent -> critical.
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -12,11 +12,15 @@ export function parseRequirements(md) {
|
|
|
12
12
|
const matches = [...text.matchAll(REQ_RE)];
|
|
13
13
|
const reqs = [];
|
|
14
14
|
for (let i = 0; i < matches.length; i += 1) {
|
|
15
|
-
const
|
|
15
|
+
const raw = matches[i][1].trim();
|
|
16
|
+
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"; else the whole text.
|
|
17
|
+
const idM = raw.match(/^([A-Z][A-Z0-9]*-\d+)\s*—\s*(.+)$/);
|
|
18
|
+
const id = idM ? idM[1] : null;
|
|
19
|
+
const name = idM ? idM[2].trim() : raw;
|
|
16
20
|
const start = matches[i].index + matches[i][0].length;
|
|
17
21
|
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
|
18
22
|
const body = text.slice(start, end).replace(/(\n>[^\n]*)+\s*$/, '').trim();
|
|
19
|
-
reqs.push({ name, body });
|
|
23
|
+
reqs.push({ id, name, body });
|
|
20
24
|
}
|
|
21
25
|
return reqs;
|
|
22
26
|
}
|
|
@@ -33,35 +37,36 @@ export function parseDelta(md) {
|
|
|
33
37
|
return {
|
|
34
38
|
added: parseRequirements(grab('ADDED')),
|
|
35
39
|
modified: parseRequirements(grab('MODIFIED')),
|
|
36
|
-
removed: parseRequirements(grab('REMOVED')).map((r) => r.name),
|
|
40
|
+
removed: parseRequirements(grab('REMOVED')).map((r) => r.id || r.name),
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export function applyDelta(reqs, delta) {
|
|
41
|
-
const
|
|
42
|
-
const
|
|
45
|
+
const keyOf = (r) => r.id || r.name; // identity = ID when present, else the name
|
|
46
|
+
const order = reqs.map(keyOf);
|
|
47
|
+
const map = new Map(reqs.map((r) => [keyOf(r), r])); // key -> full {id,name,body}
|
|
43
48
|
const warnings = [];
|
|
44
49
|
for (const r of delta.added || []) {
|
|
45
|
-
|
|
46
|
-
map.
|
|
50
|
+
const k = keyOf(r);
|
|
51
|
+
if (map.has(k)) warnings.push(`ADDED já existe: ${k}`); else order.push(k);
|
|
52
|
+
map.set(k, r);
|
|
47
53
|
}
|
|
48
54
|
for (const r of delta.modified || []) {
|
|
49
|
-
|
|
50
|
-
map.
|
|
55
|
+
const k = keyOf(r);
|
|
56
|
+
if (!map.has(k)) { warnings.push(`MODIFIED inexistente: ${k}`); order.push(k); }
|
|
57
|
+
map.set(k, r);
|
|
51
58
|
}
|
|
52
|
-
for (const
|
|
53
|
-
if (!map.has(
|
|
54
|
-
map.delete(
|
|
59
|
+
for (const k of delta.removed || []) {
|
|
60
|
+
if (!map.has(k)) warnings.push(`REMOVED inexistente: ${k}`);
|
|
61
|
+
map.delete(k);
|
|
55
62
|
}
|
|
56
63
|
const seen = new Set();
|
|
57
|
-
const out = order
|
|
58
|
-
.filter((n) => map.has(n) && !seen.has(n) && seen.add(n))
|
|
59
|
-
.map((n) => ({ name: n, body: map.get(n) }));
|
|
64
|
+
const out = order.filter((k) => map.has(k) && !seen.has(k) && seen.add(k)).map((k) => map.get(k));
|
|
60
65
|
return { reqs: out, warnings };
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export function renderSpec(capability, reqs, { footer } = {}) {
|
|
64
|
-
const blocks = reqs.map((r) => `### Requisito: ${r.name}\n${r.body}`).join('\n\n');
|
|
69
|
+
const blocks = reqs.map((r) => `### Requisito: ${r.id ? `${r.id} — ${r.name}` : r.name}\n${r.body}`).join('\n\n');
|
|
65
70
|
const foot = footer ? `\n\n> ${footer}\n` : '\n';
|
|
66
71
|
return `---\ntype: spec\ncssclasses:\n - topic-spec\ntags:\n - spec\n---\n\n# ${capability}\n\n## Requisitos\n\n${blocks}${foot}`;
|
|
67
72
|
}
|
|
@@ -95,3 +100,15 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
95
100
|
}
|
|
96
101
|
return { promoted, warnings };
|
|
97
102
|
}
|
|
103
|
+
|
|
104
|
+
// Gate check for the independent verdict (Wave A). A requirement-bearing change must have
|
|
105
|
+
// a verdict that is ok and covers every declared req id. A requirement-less change passes:
|
|
106
|
+
// nothing for an independent verifier to check — the sensor gate is already the proof.
|
|
107
|
+
export function evaluateVerdict(verdict, reqIds) {
|
|
108
|
+
const ids = reqIds || [];
|
|
109
|
+
if (ids.length === 0) return { ok: true, missing: [] };
|
|
110
|
+
if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
|
|
111
|
+
const covered = new Set((verdict.coverage || []).filter((c) => c.covered).map((c) => c.req));
|
|
112
|
+
const missing = ids.filter((r) => !covered.has(r));
|
|
113
|
+
return { ok: missing.length === 0, missing };
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"hooks",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
17
18
|
"node": ">=18"
|
|
@@ -32,5 +33,13 @@
|
|
|
32
33
|
"token-cost"
|
|
33
34
|
],
|
|
34
35
|
"author": "Roger Alves",
|
|
35
|
-
"license": "MIT"
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/rogersialves/wendkeep.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/rogersialves/wendkeep#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
44
|
+
}
|
|
36
45
|
}
|
package/src/change.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
archiveChange,
|
|
10
10
|
} from '../hooks/change-core.mjs';
|
|
11
11
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
12
|
+
import { evaluateVerdict } from '../hooks/spec-core.mjs';
|
|
12
13
|
import { getNextAdrNumber } from '../hooks/obsidian-common.mjs';
|
|
13
14
|
|
|
14
15
|
function resolveVault(argv) {
|
|
@@ -39,7 +40,7 @@ export function runChange(argv) {
|
|
|
39
40
|
if (sub === 'new') {
|
|
40
41
|
const slug = slugArg();
|
|
41
42
|
if (!slug) { process.stderr.write('wendkeep change new: missing <slug>\n'); process.exit(2); }
|
|
42
|
-
const r = newChange(vaultBase, slug, { dateStr: today() });
|
|
43
|
+
const r = newChange(vaultBase, slug, { dateStr: today(), simple: rest.includes('--simple') });
|
|
43
44
|
process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
|
|
44
45
|
process.exit(0);
|
|
45
46
|
}
|
|
@@ -70,15 +71,24 @@ export function runChange(argv) {
|
|
|
70
71
|
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
71
72
|
// Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
|
|
72
73
|
const gate = (dir) => {
|
|
73
|
-
let
|
|
74
|
-
try {
|
|
74
|
+
let tasks = [];
|
|
75
|
+
try { tasks = parseTasks(readFileSync(join(dir, 'tarefas.md'), 'utf8')); } catch { /* no tasks */ }
|
|
76
|
+
const required = requiredSensors(tasks);
|
|
77
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
75
78
|
let evidence = [];
|
|
76
79
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
77
|
-
|
|
80
|
+
const s = evaluateGate(evidence, required);
|
|
81
|
+
if (!s.ok) return s;
|
|
82
|
+
// Independent verdict (Wave A): required only when the change declares [req:] tasks.
|
|
83
|
+
let verdict = null;
|
|
84
|
+
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* none */ }
|
|
85
|
+
const v = evaluateVerdict(verdict, reqIds);
|
|
86
|
+
if (!v.ok) return { ok: false, failing: verdict ? [`verdict incompleto: falta ${v.missing.join(', ')}`] : ['sem verdict — rode `wendkeep verify --deep` + skill wk-verify'] };
|
|
87
|
+
return { ok: true, failing: [] };
|
|
78
88
|
};
|
|
79
89
|
const r = archiveChange(vaultBase, slug, { dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate });
|
|
80
90
|
if (!r.ok) {
|
|
81
|
-
process.stderr.write(`change archive BLOCKED (gate):
|
|
91
|
+
process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
|
|
82
92
|
process.exit(1);
|
|
83
93
|
}
|
|
84
94
|
process.stdout.write(`archived: ${r.archivedRel}; ADR: ${r.adrRel}\n`);
|
package/src/doctor.mjs
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
-
// `wendkeep doctor` —
|
|
2
|
-
//
|
|
3
|
-
// hooks/vault-health.mjs (which reads getVaultBase()).
|
|
1
|
+
// `wendkeep doctor` — vault/session integrity (hooks/vault-health.mjs) PLUS the a2
|
|
2
|
+
// harness integrity check (hooks/harness-doctor.mjs). Exits 1 on any error.
|
|
4
3
|
import { spawnSync } from 'node:child_process';
|
|
5
4
|
import { existsSync } from 'node:fs';
|
|
6
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { checkHarness } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
|
|
9
9
|
export function runDoctor(argv) {
|
|
10
10
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const hookFile = join(here, '..', 'hooks', 'vault-health.mjs');
|
|
12
|
-
if (!existsSync(hookFile)) {
|
|
13
|
-
process.stderr.write(`wendkeep doctor: vault-health.mjs not found at ${hookFile}\n`);
|
|
14
|
-
process.exit(2);
|
|
15
|
-
}
|
|
16
12
|
|
|
17
13
|
let vault;
|
|
14
|
+
let project;
|
|
18
15
|
const passthrough = [];
|
|
19
16
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20
17
|
const a = argv[i];
|
|
21
18
|
if (a === '--vault') vault = argv[++i];
|
|
22
19
|
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
20
|
+
else if (a === '--project') project = argv[++i];
|
|
21
|
+
else if (a.startsWith('--project=')) project = a.slice(10);
|
|
23
22
|
else passthrough.push(a);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
const env = { ...process.env };
|
|
27
26
|
if (vault) env.OBSIDIAN_VAULT_PATH = isAbsolute(vault) ? vault : resolve(process.cwd(), vault);
|
|
28
|
-
|
|
29
27
|
if (!env.OBSIDIAN_VAULT_PATH) {
|
|
30
28
|
process.stderr.write('wendkeep doctor: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
|
|
31
29
|
process.exit(2);
|
|
32
30
|
}
|
|
31
|
+
const vaultBase = env.OBSIDIAN_VAULT_PATH;
|
|
32
|
+
const projectRoot = resolve(project || process.cwd());
|
|
33
|
+
|
|
34
|
+
// 1. Session/vault integrity (existing check).
|
|
35
|
+
let healthStatus = 0;
|
|
36
|
+
if (existsSync(hookFile)) {
|
|
37
|
+
const r = spawnSync(process.execPath, [hookFile, ...passthrough], { stdio: 'inherit', env });
|
|
38
|
+
healthStatus = r.status ?? 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Harness integrity (Wave B).
|
|
42
|
+
const { errors, warnings } = checkHarness(vaultBase, projectRoot);
|
|
43
|
+
process.stdout.write(`\n[harness] ${errors.length} erro(s), ${warnings.length} aviso(s)\n`);
|
|
44
|
+
for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
|
|
45
|
+
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
process.exit(r.status ?? 0);
|
|
47
|
+
process.exit(healthStatus !== 0 || errors.length ? 1 : 0);
|
|
36
48
|
}
|
package/src/lessons.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// `wendkeep lesson add "<trigger>" "<lesson>"` — record a project-local lesson (Wave B).
|
|
2
|
+
// The wk-verify / wk-debugging skills call this when a verification fails.
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
5
|
+
|
|
6
|
+
function opt(argv, name) {
|
|
7
|
+
const i = argv.indexOf(name);
|
|
8
|
+
if (i >= 0) return argv[i + 1];
|
|
9
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
10
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function today() {
|
|
14
|
+
const d = new Date();
|
|
15
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function runLesson(argv) {
|
|
19
|
+
const [sub, ...rest] = argv;
|
|
20
|
+
if (sub !== 'add') {
|
|
21
|
+
process.stderr.write(`wendkeep lesson: unknown "${sub}". Use: lesson add "<trigger>" "<lesson>"\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
25
|
+
if (!vaultRaw) { process.stderr.write('wendkeep lesson: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
26
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
27
|
+
|
|
28
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && rest[i - 1] !== '--vault' && rest[i - 1] !== '--change');
|
|
29
|
+
const [trigger, lesson] = positional;
|
|
30
|
+
if (!trigger || !lesson) { process.stderr.write('wendkeep lesson add: precisa "<trigger>" "<lesson>"\n'); process.exit(2); }
|
|
31
|
+
|
|
32
|
+
const p = addLesson(vaultBase, { trigger, lesson, sourceChange: opt(rest, '--change') || '', dateStr: today() });
|
|
33
|
+
process.stdout.write(`lesson: ${p}\n`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
package/src/skills-seed.mjs
CHANGED
|
@@ -23,14 +23,19 @@ Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault)
|
|
|
23
23
|
A mudança vira a *ativa* (ponteiro \`.brain/CURRENT_CHANGE.md\`) e é injetada no
|
|
24
24
|
próximo SessionStart, então você retoma o trabalho em curso automaticamente.
|
|
25
25
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
26
|
-
(teste vermelho antes do código). Marque \`- [x]\` ao concluir
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
27
|
+
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
28
|
+
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
29
|
+
quando a change mexe numa capability de \`07-Specs\`.
|
|
30
|
+
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
31
|
+
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
32
|
+
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
33
|
+
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
34
|
+
re-deriva a cobertura do \`07-Specs\` e grava \`verdict.json\`. Change trivial (sem
|
|
35
|
+
\`[req:]\`) recebe verdict automático — pula este passe.
|
|
36
|
+
6. **Archive** — \`wendkeep change archive <slug>\`. O *gate* exige sensores verdes **E**
|
|
37
|
+
\`verdict.json\` cobrindo os \`[req:]\`. Passando, promove os deltas pro \`07-Specs\`,
|
|
38
|
+
move a change pro \`_arquivo\` e gera um ADR em \`04-Decisões/\`.
|
|
34
39
|
|
|
35
40
|
## Regras
|
|
36
41
|
|
|
@@ -41,25 +46,46 @@ Use ao começar qualquer mudança não-trivial. O loop mantém memória (vault)
|
|
|
41
46
|
o grafo do Obsidian mostra plano↔sessão↔decisão.
|
|
42
47
|
`;
|
|
43
48
|
|
|
44
|
-
const TDD = `# TDD — Red, Green, Refactor
|
|
49
|
+
const TDD = `# TDD — Red, Green, Refactor (testes que discriminam)
|
|
45
50
|
|
|
46
|
-
Use ao implementar qualquer comportamento. Nunca escreva código de produção sem um
|
|
47
|
-
|
|
51
|
+
Use ao implementar qualquer comportamento. Nunca escreva código de produção sem um teste
|
|
52
|
+
vermelho pedindo por ele — e nunca escreva um teste que passaria sob a implementação errada.
|
|
48
53
|
|
|
49
54
|
## O ciclo
|
|
50
55
|
|
|
51
|
-
1. **Red** —
|
|
52
|
-
2. **Rode e veja falhar** —
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
1. **Red** — o menor teste que falha por faltar o comportamento.
|
|
57
|
+
2. **Rode e veja falhar** — pelo motivo certo (não import/typo). Teste que nunca falhou não prova nada.
|
|
58
|
+
3. **Green** — o código mínimo pra passar.
|
|
59
|
+
4. **Refactor** — limpe com os verdes te protegendo.
|
|
60
|
+
|
|
61
|
+
## Derive do spec, não do código
|
|
62
|
+
|
|
63
|
+
Escreva a asserção a partir do *critério de aceite* do requisito (\`07-Specs\`), não lendo a
|
|
64
|
+
implementação. Cada asserção mira o resultado que o spec definiu. Escrever o teste lendo o
|
|
65
|
+
código = ele só confirma o que o código já faz, bugs inclusos.
|
|
66
|
+
|
|
67
|
+
## Litmus não-raso
|
|
68
|
+
|
|
69
|
+
Rejeite asserção que passaria sob uma implementação errada. Afirme **valor ou estado
|
|
70
|
+
persistido**, nunca "o mock foi chamado". "Testado depois" é rejeitado — o teste nasce
|
|
71
|
+
junto com o código da tarefa.
|
|
72
|
+
|
|
73
|
+
## Adequação (necessário e suficiente)
|
|
74
|
+
|
|
75
|
+
- Todo critério de aceite tem uma asserção — com evidência \`arquivo:linha\`.
|
|
76
|
+
- Todo teste rastreia um requisito (\`[req:ID]\`). Teste sem requisito = escopo à toa.
|
|
77
|
+
- Nem raso, nem inflado: cobre o que o spec pede, nada além.
|
|
78
|
+
|
|
79
|
+
## Aprende o projeto
|
|
80
|
+
|
|
81
|
+
Antes de escrever, amostre 5–10 testes existentes: estilo, localização, framework,
|
|
82
|
+
profundidade por camada — trate essa profundidade como *piso*, nunca teto. Leia
|
|
83
|
+
\`AGENTS.md\` / \`.cursor/rules\` / configs de CI pros padrões declarados.
|
|
56
84
|
|
|
57
85
|
## Regras
|
|
58
86
|
|
|
59
|
-
- Um comportamento por teste
|
|
60
|
-
-
|
|
61
|
-
- Não teste detalhe interno que você vai refatorar — teste a interface observável.
|
|
62
|
-
- Bug encontrado = primeiro um teste que o reproduz (vermelho), depois a correção (verde).
|
|
87
|
+
- Um comportamento por teste; o nome descreve o comportamento, não a implementação.
|
|
88
|
+
- Bug encontrado = primeiro o teste que o reproduz (vermelho), depois a correção.
|
|
63
89
|
`;
|
|
64
90
|
|
|
65
91
|
const DEBUGGING = `# Depuração sistemática
|
|
@@ -99,6 +125,17 @@ qualquer código.
|
|
|
99
125
|
3. **Proponha 2–3 abordagens** — com trade-offs e sua recomendação primeiro.
|
|
100
126
|
4. **Apresente o design em seções** — escale ao tamanho do problema; confirme cada seção.
|
|
101
127
|
|
|
128
|
+
## Closure gate — nada de ambiguidade solta
|
|
129
|
+
|
|
130
|
+
Antes de fechar o design, resolva cada zona cinza: decide com o usuário, ou registra como
|
|
131
|
+
**assumption assinada** ("assumo X porque Y — corrija se errado"). Cinza declinado pelo
|
|
132
|
+
usuário é *registrado*, não descartado no silêncio. Nada sai do design silenciosamente ambíguo.
|
|
133
|
+
|
|
134
|
+
## Tabela out-of-scope
|
|
135
|
+
|
|
136
|
+
Liste explicitamente o que a mudança **não** faz. Escopo não declarado vira creep; a tabela
|
|
137
|
+
é o contrato do que ficou de fora — e base pra próxima change.
|
|
138
|
+
|
|
102
139
|
## Gate rígido
|
|
103
140
|
|
|
104
141
|
NÃO escreva código, scaffold nem tome ação de implementação até apresentar um design e o
|
|
@@ -139,12 +176,38 @@ de 2–5 min:
|
|
|
139
176
|
- Nomes/assinaturas consistentes entre tarefas (uma função é \`x()\` em toda parte).
|
|
140
177
|
`;
|
|
141
178
|
|
|
179
|
+
const VERIFY = `# Verificação independente — o passe fresco
|
|
180
|
+
|
|
181
|
+
Use no passo *verify deep* do loop, depois de \`wendkeep verify --deep\`. Você é o
|
|
182
|
+
**verificador**, não o autor — mesmo tendo escrito o código, entre neste passe como se
|
|
183
|
+
nunca tivesse visto a implementação. Contexto fresco, read-only.
|
|
184
|
+
|
|
185
|
+
## O que fazer
|
|
186
|
+
|
|
187
|
+
1. Leia o pacote \`08-Mudanças/<slug>/verificacao.json\` (requisitos, tarefas, evidência).
|
|
188
|
+
2. **Re-derive a cobertura do \`07-Specs\`** — pra cada \`[req:ID]\` da change, cheque se o
|
|
189
|
+
comportamento definido no requisito está coberto por um teste que discrimina (não
|
|
190
|
+
passaria sob impl errada). Evidência \`arquivo:linha\`.
|
|
191
|
+
3. Outcome check ancorado no spec: o resultado observável bate com o critério de aceite?
|
|
192
|
+
4. Grave \`08-Mudanças/<slug>/verdict.json\`:
|
|
193
|
+
\`{ "slug": "...", "ok": true, "coverage": [{ "req": "GATE-1", "covered": true, "evidence": "arquivo:linha" }], "notes": [] }\`.
|
|
194
|
+
|
|
195
|
+
## Regras
|
|
196
|
+
|
|
197
|
+
- **Autor ≠ verificador.** No Claude, spawn um sub-agente read-only pra este passe
|
|
198
|
+
(isolamento real). Nos outros, entre num contexto limpo e re-derive do spec, não da memória.
|
|
199
|
+
- \`ok: false\` se algum requisito não tem cobertura que discrimina. Gap não é "quase lá" — é vermelho.
|
|
200
|
+
- Não conserte aqui. Gap vira tarefa de correção na change; re-verifica depois.
|
|
201
|
+
- O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
|
|
202
|
+
`;
|
|
203
|
+
|
|
142
204
|
export const WK_SKILLS = [
|
|
143
205
|
skill('wk-workflow', 'Use ao começar qualquer mudança não-trivial — orquestra o loop a2 (explore, propose, apply, verify, archive) nos comandos wendkeep.', WORKFLOW),
|
|
144
|
-
skill('wk-tdd', 'Use ao implementar qualquer comportamento —
|
|
206
|
+
skill('wk-tdd', 'Use ao implementar qualquer comportamento — Red/Green/Refactor com testes que discriminam (derivados do spec, litmus não-raso, adequação).', TDD),
|
|
145
207
|
skill('wk-debugging', 'Use quando algo falha ou quebra — depuração sistemática por hipótese antes de corrigir.', DEBUGGING),
|
|
146
|
-
skill('wk-brainstorming', 'Use quando a ideia ainda é vaga —
|
|
208
|
+
skill('wk-brainstorming', 'Use quando a ideia ainda é vaga — vira design aprovado, com closure gate e tabela out-of-scope, antes de código.', BRAINSTORMING),
|
|
147
209
|
skill('wk-planning', 'Use após um design aprovado — decompõe em plano de tarefas TDD bite-sized.', PLANNING),
|
|
210
|
+
skill('wk-verify', 'Use no verify deep — passe independente read-only (autor≠verificador) que re-deriva a cobertura do spec e grava verdict.json.', VERIFY),
|
|
148
211
|
];
|
|
149
212
|
|
|
150
213
|
// Seed each skill into <brainDir>/skills/<name>/SKILL.md if absent (non-destructive).
|
package/src/taxonomy.mjs
CHANGED
package/src/verify.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
-
import { parseTasks, activeChange } from '../hooks/change-core.mjs';
|
|
6
|
+
import { parseTasks, activeChange, appendFixTasks } from '../hooks/change-core.mjs';
|
|
7
7
|
import { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
|
|
9
9
|
function opt(argv, name) {
|
|
@@ -31,6 +31,22 @@ export function runVerify(argv) {
|
|
|
31
31
|
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
32
32
|
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
33
33
|
|
|
34
|
+
// Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate.
|
|
35
|
+
const withSurvivors = evidence.filter((e) => e.survivors && e.survivors.length);
|
|
36
|
+
if (withSurvivors.length) {
|
|
37
|
+
const roundFile = join(changeDir, '.mutation-round');
|
|
38
|
+
let round = 0;
|
|
39
|
+
try { round = Number(readFileSync(roundFile, 'utf8').trim()) || 0; } catch { /* first round */ }
|
|
40
|
+
if (round >= 3) {
|
|
41
|
+
process.stderr.write('verify: mutantes ainda sobrevivem após 3 rodadas — revise os testes à mão.\n');
|
|
42
|
+
} else {
|
|
43
|
+
let added = 0;
|
|
44
|
+
for (const e of withSurvivors) added += appendFixTasks(changeDir, e.survivors, e.id);
|
|
45
|
+
writeFileSync(roundFile, String(round + 1), 'utf8');
|
|
46
|
+
process.stdout.write(`verify: ${added} fix-task(s) de mutação (rodada ${round + 1}/3)\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
// Same rule as the archive gate: evidence carries severity, so evaluateGate blocks
|
|
35
51
|
// only on critical/missing — a red warning is advisory and passes verify.
|
|
36
52
|
const { ok, failing } = evaluateGate(evidence, ids);
|
|
@@ -39,6 +55,27 @@ export function runVerify(argv) {
|
|
|
39
55
|
process.stdout.write(` ${mark} ${e.id}${e.severity === 'warning' && e.status === 'red' ? ' (warning)' : ''}\n`);
|
|
40
56
|
}
|
|
41
57
|
if (!ok) { process.stderr.write(`verify: critical sensors red: ${failing.join(', ')}\n`); process.exit(1); }
|
|
58
|
+
|
|
59
|
+
// --deep (Q2=B): assemble the verification package the wk-verify skill judges. A trivial
|
|
60
|
+
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
61
|
+
if (argv.includes('--deep')) {
|
|
62
|
+
const tasks = parseTasks(tarefas);
|
|
63
|
+
const reqIds = [...new Set(tasks.map((t) => t.req).filter(Boolean))];
|
|
64
|
+
const pkg = {
|
|
65
|
+
slug,
|
|
66
|
+
requirements: reqIds.map((id) => ({ id })),
|
|
67
|
+
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
68
|
+
sensors: evidence,
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
71
|
+
if (reqIds.length === 0) {
|
|
72
|
+
writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
|
|
73
|
+
process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
|
|
74
|
+
} else {
|
|
75
|
+
process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');
|
|
76
|
+
}
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
42
79
|
process.stdout.write(`verify OK (${ids.length} sensor(s))\n`);
|
|
43
80
|
process.exit(0);
|
|
44
81
|
}
|