wendkeep 0.39.0 → 0.40.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 +27 -0
- package/bin/wendkeep.mjs +7 -0
- package/hooks/change-core.mjs +7 -7
- package/hooks/harness-doctor.mjs +1 -1
- package/hooks/sensors-core.mjs +28 -6
- package/hooks/spec-core.mjs +17 -3
- package/package.json +2 -2
- package/src/change.mjs +5 -5
- package/src/import.mjs +15 -0
- package/src/skills-seed.mjs +9 -3
- package/src/spec.mjs +1 -1
- package/src/verify.mjs +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.40.0] — 2026-07-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `parseTasks` captura **todos** os `[req:]` de uma tarefa em `reqs: string[]` (`req` permanece
|
|
12
|
+
como alias do primeiro, retrocompatível). Antes, só o primeiro entrava no pacote de
|
|
13
|
+
verificação e os demais sumiam sem aviso.
|
|
14
|
+
- Heading de requisito aceita ID puro (`### Requisito: GATE-1`) como identidade, além do
|
|
15
|
+
formato preferido `### Requisito: <ID> — <nome>`. Diagnóstico de requisito órfão agora
|
|
16
|
+
ensina o formato esperado com exemplo concreto.
|
|
17
|
+
- `findProjectRoot`: `wendkeep verify` executado de um subdiretório sobe a árvore até achar
|
|
18
|
+
`wendkeep.sensors.json`/`.wendkeep.json` (à la `.git`); `--project` continua autoritativo.
|
|
19
|
+
- `--help`/`-h` universal: qualquer subcomando com `--help` imprime a ajuda e sai com 0,
|
|
20
|
+
interceptado antes da resolução de vault — nunca executa o comando.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Regex de ID de requisito unificada entre tarefa e spec (`REQ_ID_RE_SRC`): IDs
|
|
25
|
+
multi-segmento (`API-AUTH-2`) agora são reconhecidos também nas tarefas.
|
|
26
|
+
- `wendkeep verify` distingue `wendkeep.sensors.json` ausente (aviso com path + dica
|
|
27
|
+
`--project`) de JSON inválido (erro alto com a mensagem do parse). Antes, ambos viravam
|
|
28
|
+
"sensor não definido" para todos os sensores.
|
|
29
|
+
- `wendkeep import` com flag desconhecida agora falha com exit 2 citando a flag, em vez de
|
|
30
|
+
cair no default destrutivo `--source all` (que chegou a importar 78 sessões sem querer).
|
|
31
|
+
- Templates seed (skills de workflow pt/en) documentam o formato de heading de requisito e o
|
|
32
|
+
suporte a múltiplos `[req:]` por tarefa.
|
|
33
|
+
|
|
7
34
|
## [0.39.0] — 2026-07-13
|
|
8
35
|
|
|
9
36
|
### Added
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -125,6 +125,13 @@ async function preferProjectVault(argv) {
|
|
|
125
125
|
|
|
126
126
|
async function main() {
|
|
127
127
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
128
|
+
// Universal --help: any subcommand with --help/-h prints usage and never executes.
|
|
129
|
+
// Intercepted BEFORE vault resolution so it works anywhere — help must never depend
|
|
130
|
+
// on project state, and no command may treat --help as a runnable default.
|
|
131
|
+
if (cmd && (rest.includes('--help') || rest.includes('-h'))) {
|
|
132
|
+
process.stdout.write(HELP);
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
128
135
|
if (cmd && !['init', 'hook', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
129
136
|
await preferProjectVault(rest);
|
|
130
137
|
}
|
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, captureSpecBaseline } from './spec-core.mjs';
|
|
7
|
+
import { parseSpecsList, promoteSpecs, discoverSpecDeltas, tasksHashOf, captureSpecBaseline, REQ_ID_RE_SRC } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
10
10
|
export const ARCHIVE_DIR = '_arquivo';
|
|
@@ -161,18 +161,18 @@ export function parseTasks(md) {
|
|
|
161
161
|
const tasks = [];
|
|
162
162
|
const re = /^-\s+\[( |x)\]\s+(\S+)\s+(.*)$/gm;
|
|
163
163
|
const sensorRe = /\[sensor:\s*([\w.-]+)\]/;
|
|
164
|
-
const
|
|
164
|
+
const reqReG = new RegExp(`\\[req:\\s*(${REQ_ID_RE_SRC})\\]`, 'g');
|
|
165
165
|
let m;
|
|
166
166
|
while ((m = re.exec(String(md))) !== null) {
|
|
167
167
|
let text = m[3].trim();
|
|
168
168
|
const sm = text.match(sensorRe);
|
|
169
|
-
const
|
|
169
|
+
const reqs = [...text.matchAll(reqReG)].map((r) => r[1]);
|
|
170
170
|
const sensor = sm ? sm[1] : undefined;
|
|
171
|
-
const req = rm ? rm[1] : undefined;
|
|
172
171
|
if (sm) text = text.replace(sensorRe, '');
|
|
173
|
-
if (
|
|
172
|
+
if (reqs.length) text = text.replace(reqReG, '');
|
|
174
173
|
text = text.replace(/\s+/g, ' ').trim();
|
|
175
|
-
|
|
174
|
+
// `req` stays as alias of the first id — older consumers keep working.
|
|
175
|
+
tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(reqs.length ? { req: reqs[0], reqs } : {}) });
|
|
176
176
|
}
|
|
177
177
|
return tasks;
|
|
178
178
|
}
|
|
@@ -419,7 +419,7 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
let reqIds = [];
|
|
422
|
-
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).
|
|
422
|
+
try { reqIds = [...new Set(parseTasks(readFileSync(join(src, 'tarefas.md'), 'utf8')).flatMap((t) => t.reqs ?? []))]; } catch { /* sem tarefas */ }
|
|
423
423
|
|
|
424
424
|
ensureDir(join(vaultBase, chDir, ARCHIVE_DIR));
|
|
425
425
|
try {
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
41
41
|
let tasks = [];
|
|
42
42
|
let tarefasMd = '';
|
|
43
43
|
try { tarefasMd = readFileSync(join(dir, 'tarefas.md'), 'utf8'); tasks = parseTasks(tarefasMd); } catch { /* sem tarefas */ }
|
|
44
|
-
const reqIds = [...new Set(tasks.
|
|
44
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
45
45
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
46
46
|
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
47
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`);
|
package/hooks/sensors-core.mjs
CHANGED
|
@@ -2,15 +2,37 @@
|
|
|
2
2
|
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
3
|
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
|
|
9
|
+
return loadSensorsDetailed(projectRoot, file).sensors;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Missing config and broken config are different failures: absent file usually means
|
|
13
|
+
// wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
|
|
14
|
+
// both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
|
|
15
|
+
export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
|
|
16
|
+
const path = join(projectRoot, file);
|
|
17
|
+
if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
|
|
9
18
|
try {
|
|
10
|
-
const data = JSON.parse(readFileSync(
|
|
11
|
-
return Array.isArray(data.sensors) ? data.sensors : [];
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
19
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { sensors: [], missing: false, error: e.message, path };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Climb the directory tree looking for a project marker (wendkeep.sensors.json or
|
|
27
|
+
// .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
|
|
28
|
+
// across commands, so verify is often run from a subdirectory.
|
|
29
|
+
export function findProjectRoot(startDir) {
|
|
30
|
+
let dir = resolve(startDir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) return null;
|
|
35
|
+
dir = parent;
|
|
14
36
|
}
|
|
15
37
|
}
|
|
16
38
|
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -22,15 +22,29 @@ export const MANAGED_SPEC_MARKER = '<!-- wendkeep:managed-spec — generated fro
|
|
|
22
22
|
// Parse is BILINGUAL always (mixed vaults never break); rendering follows the vault locale.
|
|
23
23
|
const REQ_RE = /^### (?:Requisito|Requirement):\s*(.+)$/gm;
|
|
24
24
|
|
|
25
|
+
// Single source of truth for requirement-id shape — task tags ([req:ID]) and spec
|
|
26
|
+
// headings MUST agree, or coverage silently misses (multi-segment ids like API-AUTH-2).
|
|
27
|
+
export const REQ_ID_RE_SRC = '[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\\d+';
|
|
28
|
+
|
|
29
|
+
// Orphan diagnostics must teach the fix, not just name the ids — the heading format
|
|
30
|
+
// is the most common cause and lives only here.
|
|
31
|
+
export function formatOrphanReqs(ids) {
|
|
32
|
+
const list = ids.join(', ');
|
|
33
|
+
const ex = ids[0] || 'GATE-1';
|
|
34
|
+
return `requisito(s) órfão(s) na spec efetiva: ${list} — heading esperado no spec.md da change: "### Requisito: ${ex} — <nome>" (ou só "### Requisito: ${ex}")`;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export function parseRequirements(md) {
|
|
26
38
|
const text = String(md);
|
|
27
39
|
const matches = [...text.matchAll(REQ_RE)];
|
|
28
40
|
const reqs = [];
|
|
29
41
|
for (let i = 0; i < matches.length; i += 1) {
|
|
30
42
|
const raw = matches[i][1].trim();
|
|
31
|
-
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>"
|
|
32
|
-
|
|
33
|
-
const
|
|
43
|
+
// Identity is the ID (e.g. GATE-1) when the heading is "<ID> — <nome>" or a bare
|
|
44
|
+
// "<ID>"; else the whole text. Bare ids keep specs writable without the em-dash.
|
|
45
|
+
const idM = raw.match(new RegExp(`^(${REQ_ID_RE_SRC})\\s*—\\s*(.+)$`));
|
|
46
|
+
const bare = idM ? null : raw.match(new RegExp(`^(${REQ_ID_RE_SRC})$`));
|
|
47
|
+
const id = idM ? idM[1] : bare ? bare[1] : null;
|
|
34
48
|
const name = idM ? idM[2].trim() : raw;
|
|
35
49
|
const start = matches[i].index + matches[i][0].length;
|
|
36
50
|
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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": {
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"url": "https://github.com/rogersialves/wendkeep/issues"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"wendkeep": "^0.
|
|
49
|
+
"wendkeep": "^0.39.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/change.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
scaffoldPlaceholders,
|
|
18
18
|
} from '../hooks/change-core.mjs';
|
|
19
19
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
20
|
-
import { buildEffectiveRequirementPackage, evaluateVerdict, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
20
|
+
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
21
21
|
import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
22
22
|
import { getLocale } from '../hooks/locale.mjs';
|
|
23
23
|
|
|
@@ -147,13 +147,13 @@ export function runChange(argv) {
|
|
|
147
147
|
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
148
148
|
process.stdout.write(`tarefas: ${done} done / ${tasks.length - done} open\n`);
|
|
149
149
|
for (const t of tasks) {
|
|
150
|
-
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${t.
|
|
150
|
+
process.stdout.write(` [${t.done ? 'x' : ' '}] ${t.id} ${t.text}${(t.reqs ?? []).map((r) => ` [req:${r}]`).join('')}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
|
|
151
151
|
}
|
|
152
152
|
let evidence = null;
|
|
153
153
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
154
154
|
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
155
155
|
else process.stdout.write('evidencia: ausente\n');
|
|
156
|
-
const reqIds = [...new Set(tasks.
|
|
156
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
157
157
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
158
158
|
if (effective.errors.length || effective.missing.length) {
|
|
159
159
|
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
@@ -239,10 +239,10 @@ export function runChange(argv) {
|
|
|
239
239
|
return { ok: false, failing: ['evidência stale (tarefas.md mudou desde o último verify) — rode `wendkeep verify` de novo'] };
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
const reqIds = [...new Set(tasks.
|
|
242
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
243
243
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
244
244
|
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
245
|
-
if (effective.missing.length) return { ok: false, failing: [
|
|
245
|
+
if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
|
|
246
246
|
let evidence = [];
|
|
247
247
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
248
248
|
const s = evaluateGate(evidence, required);
|
package/src/import.mjs
CHANGED
|
@@ -12,7 +12,22 @@ function opt(argv, name) {
|
|
|
12
12
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const KNOWN_FLAGS = new Set([
|
|
16
|
+
'--vault', '--project', '--source', '--from', '--codex-from', '--since', '--limit',
|
|
17
|
+
'--dry-run', '--json', '--rescan-decisions', '--stamp-ids', '--help', '-h',
|
|
18
|
+
]);
|
|
19
|
+
|
|
15
20
|
export function runImportCli(argv) {
|
|
21
|
+
// Import writes to the vault; an unrecognized flag must never fall through to the
|
|
22
|
+
// destructive default (--source all). Fail fast, point at --help.
|
|
23
|
+
for (const a of argv) {
|
|
24
|
+
if (!a.startsWith('-')) continue;
|
|
25
|
+
const name = a.includes('=') ? a.slice(0, a.indexOf('=')) : a;
|
|
26
|
+
if (!KNOWN_FLAGS.has(name)) {
|
|
27
|
+
process.stderr.write(`wendkeep import: flag desconhecida "${name}" (use --help)\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
17
32
|
if (!vaultRaw) { process.stderr.write('wendkeep import: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
18
33
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
package/src/skills-seed.mjs
CHANGED
|
@@ -35,15 +35,18 @@ vault cego. Exceção única: mudança trivial (typo, 1 linha).
|
|
|
35
35
|
Antes de implementar, resolva \`spec_impact\` na proposta:
|
|
36
36
|
- \`required\`: liste a capability em \`specs:\` e preencha
|
|
37
37
|
\`specs/<capability>/spec.md\` com ADDED/MODIFIED/REMOVED; ligue tarefas com \`[req:ID]\`.
|
|
38
|
+
Heading de requisito: \`### Requisito: <ID> — <nome>\` (ou só \`### Requisito: <ID>\`);
|
|
39
|
+
o ID é a identidade (ex.: \`GATE-1\`, \`API-AUTH-2\`).
|
|
38
40
|
- \`none\`: registre uma justificativa real em \`spec_impact_reason\`.
|
|
39
41
|
\`pending\` nunca é estado pronto para implementação ou archive.
|
|
40
42
|
3. **Apply** — implemente cada tarefa de \`tarefas.md\` com disciplina **wk-tdd**
|
|
41
43
|
(teste vermelho antes do código). Marque \`- [x]\` ao concluir. Declare nas tarefas:
|
|
42
44
|
- \`[sensor:<id>]\` — a prova automatizada (roda no verify).
|
|
43
45
|
- \`[req:<ID>]\` — o requisito do spec que a tarefa satisfaz (ex.: \`[req:GATE-1]\`),
|
|
44
|
-
quando a change mexe numa capability.
|
|
46
|
+
quando a change mexe numa capability. Uma tarefa pode declarar vários
|
|
47
|
+
\`[req:]\` — todos contam na cobertura. Toda autoria de spec ocorre somente em
|
|
45
48
|
\`08-Mudanças/<slug>/specs/<capability>/spec.md\`; \`07-Specs\` é gerado/read-only.
|
|
46
|
-
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [sensor:memory-validation]\`.
|
|
49
|
+
Ex.: \`- [ ] 2.1 valida CORE [req:MEM-1] [req:MEM-2] [sensor:memory-validation]\`.
|
|
47
50
|
4. **Verify** — \`wendkeep verify\` roda os sensores → \`evidencia.json\`. Depois
|
|
48
51
|
\`wendkeep verify --deep\` monta o *pacote de verificação* pro passe independente.
|
|
49
52
|
5. **Verify deep** — a skill **wk-verify** (passe fresco, autor≠verificador) lê o pacote,
|
|
@@ -259,8 +262,11 @@ leaves the vault blind. Single exception: a trivial change (typo, one line).
|
|
|
259
262
|
Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
|
|
260
263
|
\`specs:\` plus a real \`specs/<capability>/spec.md\` delta and \`[req:ID]\` links; \`none\`
|
|
261
264
|
needs a real \`spec_impact_reason\`. \`pending\` is never ready for implementation/archive.
|
|
265
|
+
Requirement heading: \`### Requirement: <ID> — <name>\` (or bare \`### Requirement: <ID>\`);
|
|
266
|
+
the ID is the identity (e.g. \`GATE-1\`, \`API-AUTH-2\`).
|
|
262
267
|
3. **Apply** — implement each task in tarefas.md with **wk-tdd** (red test first). Tag tasks:
|
|
263
|
-
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies
|
|
268
|
+
\`[sensor:<id>]\` (automated proof) and \`[req:<ID>]\` (the spec requirement it satisfies;
|
|
269
|
+
a task may declare several \`[req:]\` tags — all of them count toward coverage).
|
|
264
270
|
Author specs only in \`08-Changes/<slug>/specs/\`; \`07-Specs\` is generated/read-only.
|
|
265
271
|
4. **Verify** — \`wendkeep verify\` runs the sensors; then \`wendkeep verify --deep\` builds
|
|
266
272
|
the verification package.
|
package/src/spec.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export function runSpec(argv) {
|
|
|
49
49
|
tasks = parseTasks(readFileSync(join(changeDir, 'tarefas.md'), 'utf8'));
|
|
50
50
|
}
|
|
51
51
|
catch { process.stderr.write(`wendkeep spec effective: change not found: ${slug}\n`); process.exit(2); }
|
|
52
|
-
const reqIds = [...new Set(tasks.
|
|
52
|
+
const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
|
|
53
53
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
54
54
|
if (effective.errors.length) {
|
|
55
55
|
process.stderr.write(`wendkeep spec effective: invalid delta: ${effective.errors.join('; ')}\n`);
|
package/src/verify.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
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
|
-
import {
|
|
7
|
+
import { loadSensorsDetailed, findProjectRoot, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
|
|
8
8
|
import {
|
|
9
9
|
buildEffectiveRequirementPackage,
|
|
10
10
|
captureSpecBaseline,
|
|
11
|
+
formatOrphanReqs,
|
|
11
12
|
tasksHashOf,
|
|
12
13
|
} from '../hooks/spec-core.mjs';
|
|
13
14
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
@@ -29,7 +30,9 @@ export function runVerify(argv) {
|
|
|
29
30
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
30
31
|
if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
31
32
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
32
|
-
|
|
33
|
+
// --project wins; otherwise climb from cwd to the nearest project marker (agent shells
|
|
34
|
+
// keep their cwd across commands, so verify from a subdirectory is a recurring miss).
|
|
35
|
+
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
33
36
|
const slug = opt(argv, '--change') || activeChange(vaultBase);
|
|
34
37
|
if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
|
|
35
38
|
|
|
@@ -39,7 +42,15 @@ export function runVerify(argv) {
|
|
|
39
42
|
catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
|
|
40
43
|
|
|
41
44
|
const ids = requiredSensors(parseTasks(tarefas));
|
|
42
|
-
const
|
|
45
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
46
|
+
if (loaded.error) {
|
|
47
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json inválido em ${loaded.path}: ${loaded.error}\n`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
if (loaded.missing && ids.length) {
|
|
51
|
+
process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
|
|
52
|
+
}
|
|
53
|
+
const sensors = loaded.sensors;
|
|
43
54
|
const evidence = runSensors(sensors, ids, { cwd: projectRoot });
|
|
44
55
|
writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
|
|
45
56
|
// Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
|
|
@@ -90,7 +101,7 @@ export function runVerify(argv) {
|
|
|
90
101
|
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
91
102
|
if (argv.includes('--deep')) {
|
|
92
103
|
const tasks = parseTasks(tarefas);
|
|
93
|
-
const reqIds = [...new Set(tasks.
|
|
104
|
+
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
94
105
|
const tasksHash = tasksHashOf(tarefas);
|
|
95
106
|
captureSpecBaseline(vaultBase, changeDir);
|
|
96
107
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
@@ -99,7 +110,7 @@ export function runVerify(argv) {
|
|
|
99
110
|
process.exit(1);
|
|
100
111
|
}
|
|
101
112
|
if (effective.missing.length) {
|
|
102
|
-
process.stderr.write(`verify --deep:
|
|
113
|
+
process.stderr.write(`verify --deep: ${formatOrphanReqs(effective.missing)}\n`);
|
|
103
114
|
process.exit(1);
|
|
104
115
|
}
|
|
105
116
|
const pkg = {
|
|
@@ -116,7 +127,7 @@ export function runVerify(argv) {
|
|
|
116
127
|
body: req.body,
|
|
117
128
|
};
|
|
118
129
|
}),
|
|
119
|
-
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, done: t.done })),
|
|
130
|
+
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
|
|
120
131
|
sensors: evidence,
|
|
121
132
|
};
|
|
122
133
|
writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|