wendkeep 0.77.0 → 0.78.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.
@@ -281,7 +281,8 @@ nunca tivesse visto a implementação. Contexto fresco, read-only.
281
281
  (isolamento real). Nos outros, entre num contexto limpo e re-derive do spec, não da memória.
282
282
  - \`ok: false\` se algum requisito não tem cobertura que discrimina. Gap não é "quase lá" — é vermelho.
283
283
  - Não conserte aqui. Gap vira tarefa de correção na change; re-verifica depois.
284
- - O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\`. Sem isso, não arquiva.
284
+ - O gate do \`archive\` **exige** \`verdict.json\` com \`ok\` cobrindo todo \`[req:]\` e copiando
285
+ \`evidenceEnvelopeId\` + \`evidenceBinding\` de \`verificacao.json\`. Sem isso, não arquiva.
285
286
 
286
287
  ## Templates (nesta pasta)
287
288
  - \`spec-reviewer-prompt.md\` — cole ao spawnar o subagente verificador (read-only, autor≠verificador).
@@ -489,7 +490,8 @@ the author — even if you wrote the code, enter as if you'd never seen it. Fres
489
490
  - **Author ≠ verifier.** On Claude, spawn a read-only sub-agent for real isolation.
490
491
  - \`ok: false\` if any requirement lacks discriminating coverage. A gap is red, not "almost".
491
492
  - Don't fix here — a gap becomes a fix task; re-verify after.
492
- - The archive gate **requires** a fresh \`verdict.json\` (matching \`tasksHash\` and \`effectiveSpecHash\`) covering every \`[req:]\`.
493
+ - The archive gate **requires** a fresh \`verdict.json\` matching \`tasksHash\`,
494
+ \`effectiveSpecHash\`, \`evidenceEnvelopeId\`, and \`evidenceBinding\`, covering every \`[req:]\`.
493
495
 
494
496
  ## Templates (in this folder)
495
497
  - \`spec-reviewer-prompt.md\` — hand it to the verifier sub-agent you spawn (read-only, author≠verifier).
@@ -507,6 +509,8 @@ const VERDICT_TEMPLATE = `{
507
509
  ],
508
510
  "tasksHash": "<copie de verificacao.json — selo de frescor / copy from verificacao.json — freshness seal>",
509
511
  "effectiveSpecHash": "<copie de verificacao.json / copy from verificacao.json>",
512
+ "evidenceEnvelopeId": "<copie de verificacao.json / copy from verificacao.json>",
513
+ "evidenceBinding": "<copie o objeto de verificacao.json / copy the object from verificacao.json>",
510
514
  "notes": []
511
515
  }
512
516
  `;
@@ -532,7 +536,8 @@ Para cada \`[req:ID]\` da mudança:
532
536
 
533
537
  Grave \`08-Mudanças/<slug>/verdict.json\` no formato de \`verdict-template.json\`. \`ok: false\` se
534
538
  qualquer \`[req:]\` não tem cobertura que discrimina. Não conserte aqui — gap vira tarefa de
535
- correção. \`tasksHash\` e \`effectiveSpecHash\` vêm do pacote; alterações posteriores deixam o verdict stale.
539
+ correção. \`tasksHash\`, \`effectiveSpecHash\`, \`evidenceEnvelopeId\` e \`evidenceBinding\` vêm do
540
+ pacote; alterações posteriores deixam o verdict stale e o binding nunca deve ser reconstruído à mão.
536
541
  ---
537
542
  `;
538
543
 
@@ -556,8 +561,9 @@ For each \`[req:ID]\`:
556
561
  4. Check the observable result against the criterion — not the code.
557
562
 
558
563
  Write \`08-Changes/<slug>/verdict.json\` in the shape of \`verdict-template.json\`. \`ok: false\` if any
559
- \`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`
560
- and \`effectiveSpecHash\` come from the package (freshness seals; later task/spec edits make verdict stale).
564
+ \`[req:]\` lacks discriminating coverage. Don't fix here — a gap becomes a fix task. \`tasksHash\`,
565
+ \`effectiveSpecHash\`, \`evidenceEnvelopeId\`, and \`evidenceBinding\` are copied from the package;
566
+ never reconstruct the binding by hand (later checkout/task/spec edits make the verdict stale).
561
567
  ---
562
568
  `;
563
569
 
package/src/verify.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // `wendkeep verify [--change <slug>]` — run a change's task sensors, record evidence.
2
2
  // Sensors run at the PROJECT root (--project or cwd); the change + evidence live in
3
3
  // the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
4
- import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { readFileSync, unlinkSync } from 'node:fs';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { parseTasks, activeChange, appendFixTasks, healSpecBacklinks } from '../hooks/change-core.mjs';
7
7
  import {
@@ -21,6 +21,15 @@ import {
21
21
  import { addLesson } from '../hooks/lessons-core.mjs';
22
22
  import { getLocale } from '../hooks/locale.mjs';
23
23
  import { resolveCommandActiveContext } from './active-context-runtime.mjs';
24
+ import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
25
+ import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
26
+ import {
27
+ assertStableHead,
28
+ buildEvidenceEnvelope,
29
+ captureGitSnapshot,
30
+ resolveEvidenceIdentity,
31
+ sensorConfigSha256,
32
+ } from './evidence-envelope.mjs';
24
33
 
25
34
  function today() {
26
35
  const d = new Date();
@@ -34,6 +43,23 @@ function opt(argv, name) {
34
43
  return eq ? eq.slice(name.length + 1) : undefined;
35
44
  }
36
45
 
46
+ export function createChangeAuthorityWriter(vaultBase, changeDir, {
47
+ writeAtomic = writeVaultFileAtomic,
48
+ beforeRename,
49
+ } = {}) {
50
+ return (file, content) => writeAtomic(
51
+ vaultBase,
52
+ join(changeDir, file),
53
+ content,
54
+ 'utf8',
55
+ {
56
+ scopeRoot: changeDir,
57
+ label: `artefato de evidência ${file}`,
58
+ ...(beforeRename ? { beforeRename: (details) => beforeRename({ file, ...details }) } : {}),
59
+ },
60
+ );
61
+ }
62
+
37
63
  export function runVerify(argv) {
38
64
  const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
39
65
  if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
@@ -41,23 +67,23 @@ export function runVerify(argv) {
41
67
  // --project wins; otherwise climb from cwd to the nearest project marker (agent shells
42
68
  // keep their cwd across commands, so verify from a subdirectory is a recurring miss).
43
69
  const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
70
+ const requestedSession = opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '';
44
71
  let commandContext = null;
45
- if (!opt(argv, '--change')) {
46
- try {
47
- commandContext = resolveCommandActiveContext({
48
- vaultBase,
49
- projectRoot,
50
- sessionId: opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
51
- });
52
- } catch (error) {
53
- process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
54
- process.exit(2);
55
- }
72
+ try {
73
+ commandContext = resolveCommandActiveContext({
74
+ vaultBase,
75
+ projectRoot,
76
+ sessionId: requestedSession,
77
+ });
78
+ } catch (error) {
79
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
80
+ process.exit(2);
56
81
  }
57
82
  const slug = opt(argv, '--change') || activeChange(vaultBase, { context: commandContext });
58
83
  if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
59
84
 
60
85
  const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
86
+ const writeAuthority = createChangeAuthorityWriter(vaultBase, changeDir);
61
87
  let tarefas = '';
62
88
  try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
63
89
  catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
@@ -65,7 +91,8 @@ export function runVerify(argv) {
65
91
  // Backlink dos specs escritos à mão pro hub proposta (grafo conectado). Idempotente, fail-quiet.
66
92
  try { healSpecBacklinks(changeDir, vaultBase); } catch { /* heal é bônus */ }
67
93
 
68
- const ids = requiredSensors(parseTasks(tarefas));
94
+ const tasks = parseTasks(tarefas);
95
+ const ids = requiredSensors(tasks);
69
96
  const loaded = loadSensorsDetailed(projectRoot);
70
97
  if (loaded.error) {
71
98
  process.stderr.write(`wendkeep verify: wendkeep.sensors.json inválido em ${loaded.path}: ${loaded.error}\n`);
@@ -75,14 +102,52 @@ export function runVerify(argv) {
75
102
  process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
76
103
  }
77
104
  const sensors = loaded.sensors;
105
+ const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
106
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
107
+ const tasksHash = tasksHashOf(tarefas);
108
+ const startedAt = new Date().toISOString();
109
+ let startSnapshot;
110
+ let identity;
111
+ try {
112
+ startSnapshot = captureGitSnapshot(projectRoot);
113
+ identity = resolveEvidenceIdentity({
114
+ vaultBase,
115
+ projectRoot,
116
+ changeSlug: slug,
117
+ sessionId: requestedSession,
118
+ context: commandContext,
119
+ });
120
+ } catch (error) {
121
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
122
+ process.exit(2);
123
+ }
78
124
  const evidence = runSensors(sensors, ids, {
79
125
  cwd: projectRoot,
80
126
  env: sensorProcessEnv(vaultBase),
81
127
  });
82
- writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
128
+ let finishSnapshot;
129
+ try {
130
+ finishSnapshot = captureGitSnapshot(projectRoot);
131
+ assertStableHead(startSnapshot, finishSnapshot);
132
+ } catch (error) {
133
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
134
+ process.exit(2);
135
+ }
136
+ const envelope = buildEvidenceEnvelope({
137
+ identity,
138
+ changeSlug: slug,
139
+ snapshot: startSnapshot,
140
+ tasksSha256: tasksHash,
141
+ effectiveSpecSha256: `sha256:${effective.hash}`,
142
+ sensorConfigSha256: sensorConfigSha256(sensors, ids),
143
+ sensors: evidence,
144
+ startedAt,
145
+ finishedAt: new Date().toISOString(),
146
+ });
147
+ writeAuthority('evidencia.json', `${JSON.stringify(envelope, null, 2)}\n`);
83
148
  // Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
84
149
  // gate can reject evidence gone stale (a sensor task added after this verify run).
85
- writeFileSync(join(changeDir, '.evidence-hash'), tasksHashOf(tarefas), 'utf8');
150
+ writeAuthority('.evidence-hash', tasksHash);
86
151
 
87
152
  // Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate. A surviving
88
153
  // mutant always fails verify (exit 1): the suite does not discriminate yet. A clean report
@@ -108,7 +173,7 @@ export function runVerify(argv) {
108
173
  } else {
109
174
  let added = 0;
110
175
  for (const e of withSurvivors) added += appendFixTasks(changeDir, e.survivors, e.id);
111
- writeFileSync(roundFile, String(round + 1), 'utf8');
176
+ writeAuthority('.mutation-round', String(round + 1));
112
177
  process.stdout.write(`verify: ${added} fix-task(s) de mutação (rodada ${round + 1}/3)\n`);
113
178
  }
114
179
  process.stderr.write('verify: mutantes sobreviventes — a suíte não discrimina ainda.\n');
@@ -127,11 +192,7 @@ export function runVerify(argv) {
127
192
  // --deep (Q2=B): assemble the verification package the wk-verify skill judges. A trivial
128
193
  // change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
129
194
  if (argv.includes('--deep')) {
130
- const tasks = parseTasks(tarefas);
131
- const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
132
- const tasksHash = tasksHashOf(tarefas);
133
195
  captureSpecBaseline(vaultBase, changeDir);
134
- const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
135
196
  if (effective.errors.length) {
136
197
  process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
137
198
  process.exit(1);
@@ -144,6 +205,8 @@ export function runVerify(argv) {
144
205
  slug,
145
206
  tasksHash,
146
207
  effectiveSpecHash: effective.hash,
208
+ evidenceEnvelopeId: envelope.envelope_id,
209
+ evidenceBinding: evidenceCheckoutBinding(envelope),
147
210
  requirements: effective.requirements.map((req) => {
148
211
  return {
149
212
  id: req.id,
@@ -157,9 +220,9 @@ export function runVerify(argv) {
157
220
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
158
221
  sensors: evidence,
159
222
  };
160
- writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
223
+ writeAuthority('verificacao.json', `${JSON.stringify(pkg, null, 2)}\n`);
161
224
  if (reqIds.length === 0) {
162
- writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
225
+ writeAuthority('verdict.json', `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, evidenceEnvelopeId: envelope.envelope_id, evidenceBinding: pkg.evidenceBinding, notes: ['trivial: sem requisito'] }, null, 2)}\n`);
163
226
  process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
164
227
  } else {
165
228
  process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');