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 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
  }
@@ -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 reqRe = /\[req:\s*([A-Z][A-Z0-9]*-\d+)\]/;
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 rm = text.match(reqRe);
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 (rm) text = text.replace(reqRe, '');
172
+ if (reqs.length) text = text.replace(reqReG, '');
174
173
  text = text.replace(/\s+/g, ' ').trim();
175
- tasks.push({ id: m[2], text, done: m[1] === 'x', ...(sensor ? { sensor } : {}), ...(req ? { req } : {}) });
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')).map((t) => t.req).filter(Boolean))]; } catch { /* sem tarefas */ }
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 {
@@ -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.map((t) => t.req).filter(Boolean))];
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`);
@@ -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(join(projectRoot, file), 'utf8'));
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
 
@@ -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>"; else the whole text.
32
- const idM = raw.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+)\s*—\s*(.+)$/);
33
- const id = idM ? idM[1] : null;
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.39.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.38.3"
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.req ? ` [req:${t.req}]` : ''}${t.sensor ? ` [sensor:${t.sensor}]` : ''}\n`);
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.map((t) => t.req).filter(Boolean))];
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.map((t) => t.req).filter(Boolean))];
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: [`requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}`] };
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);
@@ -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. Toda autoria de spec ocorre somente em
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.map((task) => task.req).filter(Boolean))];
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 { loadSensors, requiredSensors, runSensors, evaluateGate } from '../hooks/sensors-core.mjs';
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
- const projectRoot = resolve(opt(argv, '--project') || process.cwd());
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 sensors = loadSensors(projectRoot);
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.map((t) => t.req).filter(Boolean))];
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: requisito(s) órfão(s) na spec efetiva: ${effective.missing.join(', ')}\n`);
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');