wendkeep 0.76.9 → 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.
package/src/change.mjs CHANGED
@@ -19,13 +19,24 @@ import {
19
19
  isGuideCompactChange,
20
20
  setActiveChange,
21
21
  } from '../hooks/change-core.mjs';
22
- import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
22
+ import { evaluateGate, loadSensorsDetailed, requiredSensors } from '../hooks/sensors-core.mjs';
23
23
  import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
24
24
  import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
25
25
  import { getLocale } from '../hooks/locale.mjs';
26
26
  import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
27
27
  import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
28
28
  import { resolveCommandActiveContext } from './active-context-runtime.mjs';
29
+ import {
30
+ captureGitSnapshot,
31
+ resolveEvidenceIdentity,
32
+ sensorConfigSha256,
33
+ } from './evidence-envelope.mjs';
34
+ import {
35
+ evaluateEvidenceBinding,
36
+ evidenceCheckoutBinding,
37
+ evidenceCheckoutBindingMatches,
38
+ evidenceSensors,
39
+ } from '../packages/vault/src/evidence-envelope.mjs';
29
40
 
30
41
  function observerMarkdownUnder(vaultBase, relativeRoot) {
31
42
  const output = [];
@@ -74,15 +85,15 @@ export function runChange(argv) {
74
85
  const VALUE_FLAGS = new Set(['--vault', '--change', '--project', '--session']);
75
86
  const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
76
87
  const projectRoot = resolve(opt(rest, '--project') || process.cwd());
88
+ const sessionId = opt(rest, '--session')
89
+ || process.env.CODEX_THREAD_ID
90
+ || process.env.CLAUDE_SESSION_ID
91
+ || '';
77
92
  let contextResolved = false;
78
93
  let resolvedContext = null;
79
94
  const context = () => {
80
95
  if (contextResolved) return resolvedContext;
81
96
  contextResolved = true;
82
- const sessionId = opt(rest, '--session')
83
- || process.env.CODEX_THREAD_ID
84
- || process.env.CLAUDE_SESSION_ID
85
- || '';
86
97
  try {
87
98
  resolvedContext = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId });
88
99
  return resolvedContext;
@@ -196,19 +207,48 @@ export function runChange(argv) {
196
207
  }
197
208
  let evidence = null;
198
209
  try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
199
- if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
210
+ if (evidence) for (const e of evidenceSensors(evidence)) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
200
211
  else process.stdout.write('evidencia: ausente\n');
201
212
  const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
202
213
  const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
203
214
  if (effective.errors.length || effective.missing.length) {
204
215
  process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
205
216
  }
217
+ if (evidence) {
218
+ let expected = {
219
+ change_slug: slug,
220
+ tasks_sha256: tasksHashOf(tarefasMd),
221
+ effective_spec_sha256: `sha256:${effective.hash}`,
222
+ };
223
+ let unavailable = '';
224
+ try {
225
+ const ids = requiredSensors(tasks);
226
+ const loaded = loadSensorsDetailed(projectRoot);
227
+ expected = {
228
+ ...expected,
229
+ identity: resolveEvidenceIdentity({
230
+ vaultBase, projectRoot, changeSlug: slug, sessionId, context: context(),
231
+ }),
232
+ snapshot: captureGitSnapshot(projectRoot),
233
+ sensor_config_sha256: sensorConfigSha256(loaded.sensors, ids),
234
+ };
235
+ } catch (error) {
236
+ unavailable = error.code || error.message;
237
+ }
238
+ const binding = evaluateEvidenceBinding(evidence, expected);
239
+ process.stdout.write(`evidence-binding: ${binding.state}${binding.reasons.length ? ` (${binding.reasons.join('; ')})` : ''}${unavailable ? ` [current snapshot unavailable: ${unavailable}]` : ''}\n`);
240
+ }
206
241
  let verdict = null;
207
242
  try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
208
243
  if (!verdict) process.stdout.write(`verdict: ausente — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ' (verdict trivial automático)'}\n`);
209
244
  else if (!reqIds.length) process.stdout.write(`verdict: ${verdict.ok === true ? 'ok (trivial)' : 'não-ok — re-verifique'}\n`);
210
245
  else {
211
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
246
+ const v = evaluateVerdict(verdict, reqIds, {
247
+ tasksHash: tasksHashOf(tarefasMd),
248
+ effectiveSpecHash: effective.hash,
249
+ evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
250
+ evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
251
+ });
212
252
  process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
213
253
  }
214
254
  try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
@@ -289,9 +329,34 @@ export function runChange(argv) {
289
329
  const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
290
330
  if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
291
331
  if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
292
- let evidence = [];
332
+ let evidence = null;
293
333
  try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
294
- const s = evaluateGate(evidence, required);
334
+ const sensorEvidence = evidenceSensors(evidence);
335
+ if (required.length && (!evidence || evidence.schema_version !== 2)) {
336
+ return { ok: false, failing: ['evidência legacy-unbound não satisfaz autoridade v2 — rode `wendkeep verify` novamente'] };
337
+ }
338
+ if (evidence?.schema_version === 2) {
339
+ let currentBinding;
340
+ try {
341
+ const loaded = loadSensorsDetailed(projectRoot);
342
+ currentBinding = evaluateEvidenceBinding(evidence, {
343
+ change_slug: slug,
344
+ identity: resolveEvidenceIdentity({
345
+ vaultBase, projectRoot, changeSlug: slug, sessionId, context: selectedContext,
346
+ }),
347
+ snapshot: captureGitSnapshot(projectRoot),
348
+ tasks_sha256: tasksHashOf(tarefasMd),
349
+ effective_spec_sha256: `sha256:${effective.hash}`,
350
+ sensor_config_sha256: sensorConfigSha256(loaded.sensors, required),
351
+ });
352
+ } catch (error) {
353
+ return { ok: false, failing: [`binding atual indisponível (${error.code || error.message}) — recupere o contexto e rode \`wendkeep verify\` novamente`] };
354
+ }
355
+ if (currentBinding.state !== 'bound') {
356
+ return { ok: false, failing: [`evidência ${currentBinding.state} (${currentBinding.reasons.join('; ')}) — rode \`wendkeep verify\` novamente`] };
357
+ }
358
+ }
359
+ const s = evaluateGate(sensorEvidence, required);
295
360
  if (!s.ok) return s;
296
361
  // Verdict SEMPRE exigido (0.31.0) — a exigência universal vive AQUI no gate; a semântica
297
362
  // reqless→ok de evaluateVerdict (spec-core) não muda porque `verify --deep` e `change
@@ -310,6 +375,19 @@ export function runChange(argv) {
310
375
  }
311
376
  let verification = null;
312
377
  try { verification = JSON.parse(readFileSync(join(dir, 'verificacao.json'), 'utf8')); } catch { /* none */ }
378
+ const checkoutBinding = evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : null;
379
+ if (evidence?.schema_version === 2 && verification?.evidenceEnvelopeId !== evidence.envelope_id) {
380
+ return { ok: false, failing: ['pacote de verificação não está ligado ao envelope atual — rode `wendkeep verify --deep` novamente'] };
381
+ }
382
+ if (checkoutBinding && !evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)) {
383
+ return { ok: false, failing: ['binding do pacote de verificação diverge do checkout provado — rode `wendkeep verify --deep` novamente'] };
384
+ }
385
+ if (evidence?.schema_version === 2 && verdict.evidenceEnvelopeId !== evidence.envelope_id) {
386
+ return { ok: false, failing: [`verdict não está ligado ao envelope atual — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
387
+ }
388
+ if (checkoutBinding && !evidenceCheckoutBindingMatches(verdict.evidenceBinding, checkoutBinding)) {
389
+ return { ok: false, failing: [`binding do verdict diverge do checkout provado — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
390
+ }
313
391
  if (verification?.effectiveSpecHash && verification.effectiveSpecHash !== effective.hash) {
314
392
  return { ok: false, failing: ['pacote de verificação stale (spec efetiva mudou) — rode `wendkeep verify --deep` novamente'] };
315
393
  }
@@ -317,7 +395,12 @@ export function runChange(argv) {
317
395
  return { ok: false, failing: ['verdict sem effectiveSpecHash — rode a skill wk-verify novamente'] };
318
396
  }
319
397
  if (reqIds.length) {
320
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: hash, effectiveSpecHash: effective.hash });
398
+ const v = evaluateVerdict(verdict, reqIds, {
399
+ tasksHash: hash,
400
+ effectiveSpecHash: effective.hash,
401
+ evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
402
+ evidenceBinding: checkoutBinding || undefined,
403
+ });
321
404
  if (!v.ok) {
322
405
  if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
323
406
  return { ok: false, failing: [`verdict incompleto: falta ${v.missing.join(', ')}`] };
@@ -0,0 +1,288 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { isUtf8 } from 'node:buffer';
4
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
7
+ import {
8
+ discoverWorktreeRepository,
9
+ readWorktreeRegistry,
10
+ worktreeIdentity,
11
+ } from '../packages/vault/src/worktree-metadata.mjs';
12
+ import {
13
+ canonicalSha256,
14
+ evaluateEvidenceBinding,
15
+ evidenceSensors,
16
+ } from '../packages/vault/src/evidence-envelope.mjs';
17
+
18
+ export { canonicalSha256, evaluateEvidenceBinding, evidenceSensors };
19
+
20
+ const PACKAGE_VERSION = JSON.parse(readFileSync(
21
+ join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
22
+ 'utf8',
23
+ )).version;
24
+
25
+ export function sensorConfigSha256(sensors, ids) {
26
+ const selected = new Set(ids || []);
27
+ const canonical = (sensors || [])
28
+ .filter((sensor) => selected.has(sensor.id))
29
+ .map((sensor) => sensor)
30
+ .sort((left, right) => String(left.id || '').localeCompare(String(right.id || '')));
31
+ return canonicalSha256(canonical);
32
+ }
33
+
34
+ function normalizedPath(value) {
35
+ return String(value || '').replaceAll('\\', '/');
36
+ }
37
+
38
+ const BINARY_EXTENSIONS = new Set([
39
+ '.7z', '.a', '.avi', '.bin', '.bmp', '.class', '.dll', '.doc', '.docx', '.dylib', '.eot',
40
+ '.exe', '.gif', '.gz', '.ico', '.jar', '.jpeg', '.jpg', '.mov', '.mp3', '.mp4', '.o',
41
+ '.ogg', '.otf', '.pdf', '.png', '.so', '.tar', '.tif', '.tiff', '.ttf', '.wav', '.webm',
42
+ '.webp', '.woff', '.woff2', '.xls', '.xlsx', '.zip',
43
+ ]);
44
+
45
+ function normalizedContent(content, binary = false) {
46
+ const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content || '');
47
+ if (binary || bytes.includes(0) || !isUtf8(bytes)) return bytes;
48
+ return Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8');
49
+ }
50
+
51
+ export function digestWorktreeEntries(entries) {
52
+ const canonical = (entries || []).map((entry) => ({
53
+ layer: String(entry.layer || ''),
54
+ status: String(entry.status || ''),
55
+ path: normalizedPath(entry.path),
56
+ ...(entry.oldPath ? { old_path: normalizedPath(entry.oldPath) } : {}),
57
+ content_mode: entry.binary ? 'binary' : 'text',
58
+ content_sha256: entry.content == null ? null : canonicalSha256(normalizedContent(entry.content, entry.binary)),
59
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
60
+ return canonicalSha256(canonical);
61
+ }
62
+
63
+ function gitResult(projectRoot, args, { spawn = spawnSync, allowFailure = false } = {}) {
64
+ const result = spawn('git', args, {
65
+ cwd: projectRoot,
66
+ encoding: null,
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ windowsHide: true,
69
+ });
70
+ if (!allowFailure && (result.error || result.status !== 0)) {
71
+ const detail = Buffer.from(result.stderr || '').toString('utf8').trim();
72
+ const error = new Error(`git ${args.join(' ')} falhou${detail ? `: ${detail}` : ''}`);
73
+ error.code = 'WENDKEEP_EVIDENCE_GIT_FAILED';
74
+ throw error;
75
+ }
76
+ return result;
77
+ }
78
+
79
+ function gitText(projectRoot, args, options) {
80
+ const result = gitResult(projectRoot, args, options);
81
+ if (result.status !== 0) return '';
82
+ return Buffer.from(result.stdout || '').toString('utf8').trim();
83
+ }
84
+
85
+ function gitBuffer(projectRoot, args, options) {
86
+ const result = gitResult(projectRoot, args, options);
87
+ return result.status === 0 ? Buffer.from(result.stdout || '') : Buffer.alloc(0);
88
+ }
89
+
90
+ function parseNameStatus(buffer, layer) {
91
+ const tokens = buffer.toString('utf8').split('\0');
92
+ if (tokens.at(-1) === '') tokens.pop();
93
+ const entries = [];
94
+ for (let index = 0; index < tokens.length;) {
95
+ const status = tokens[index++];
96
+ const renamed = /^[RC]/.test(status);
97
+ const oldPath = renamed ? tokens[index++] : '';
98
+ const path = tokens[index++];
99
+ if (!path) continue;
100
+ entries.push({ layer, status, path, ...(oldPath ? { oldPath } : {}) });
101
+ }
102
+ return entries;
103
+ }
104
+
105
+ function binaryAttributes(projectRoot, paths, options) {
106
+ const unique = [...new Set(paths.filter(Boolean))];
107
+ if (!unique.length) return new Map();
108
+ const tokens = gitBuffer(projectRoot, ['check-attr', '-z', 'binary', 'text', '--', ...unique], {
109
+ ...options, allowFailure: true,
110
+ }).toString('utf8').split('\0');
111
+ if (tokens.at(-1) === '') tokens.pop();
112
+ const attributes = new Map();
113
+ for (let index = 0; index + 2 < tokens.length; index += 3) {
114
+ const [path, attribute, value] = tokens.slice(index, index + 3);
115
+ const entry = attributes.get(path) || {};
116
+ entry[attribute] = value;
117
+ attributes.set(path, entry);
118
+ }
119
+ return attributes;
120
+ }
121
+
122
+ function pathIsBinary(path, attributes) {
123
+ const values = attributes.get(path) || {};
124
+ if (values.binary === 'set' || values.text === 'unset') return true;
125
+ if (values.binary === 'unset' || values.text === 'set' || values.text === 'auto') return false;
126
+ const normalized = normalizedPath(path).toLowerCase();
127
+ const dot = normalized.lastIndexOf('.');
128
+ return dot >= 0 && BINARY_EXTENSIONS.has(normalized.slice(dot));
129
+ }
130
+
131
+ function readWorkingPath(projectRoot, path) {
132
+ const root = resolve(projectRoot);
133
+ const absolute = resolve(root, ...normalizedPath(path).split('/'));
134
+ const scoped = relative(root, absolute);
135
+ if (scoped === '..' || scoped.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(scoped)) {
136
+ return null;
137
+ }
138
+ try {
139
+ if (!existsSync(absolute) || !statSync(absolute).isFile()) return null;
140
+ return readFileSync(absolute);
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function changedEntries(projectRoot, options) {
147
+ const staged = parseNameStatus(gitBuffer(projectRoot, [
148
+ 'diff', '--cached', '--name-status', '-z', '--find-renames', '--no-ext-diff',
149
+ ], options), 'index');
150
+ const unstaged = parseNameStatus(gitBuffer(projectRoot, [
151
+ 'diff', '--name-status', '-z', '--find-renames', '--no-ext-diff',
152
+ ], options), 'worktree');
153
+ const untracked = gitBuffer(projectRoot, [
154
+ 'ls-files', '--others', '--exclude-standard', '-z',
155
+ ], options).toString('utf8').split('\0').filter(Boolean).map((path) => ({
156
+ layer: 'untracked', status: '?', path,
157
+ }));
158
+ const attributes = binaryAttributes(projectRoot, [
159
+ ...staged.map((entry) => entry.path),
160
+ ...unstaged.map((entry) => entry.path),
161
+ ...untracked.map((entry) => entry.path),
162
+ ], options);
163
+
164
+ for (const entry of staged) {
165
+ entry.binary = pathIsBinary(entry.path, attributes);
166
+ entry.content = /^D/.test(entry.status)
167
+ ? null
168
+ : gitBuffer(projectRoot, ['show', `:${entry.path}`], { ...options, allowFailure: true });
169
+ }
170
+ for (const entry of [...unstaged, ...untracked]) {
171
+ entry.binary = pathIsBinary(entry.path, attributes);
172
+ entry.content = /^D/.test(entry.status) ? null : readWorkingPath(projectRoot, entry.path);
173
+ }
174
+ return [...staged, ...unstaged, ...untracked];
175
+ }
176
+
177
+ function resolveBaseSha(projectRoot, headSha, options) {
178
+ const upstream = gitText(projectRoot, ['rev-parse', '--verify', '@{upstream}'], {
179
+ ...options, allowFailure: true,
180
+ });
181
+ let candidate = upstream;
182
+ if (!candidate) {
183
+ candidate = gitText(projectRoot, ['rev-parse', '--verify', 'refs/heads/main'], {
184
+ ...options, allowFailure: true,
185
+ });
186
+ }
187
+ if (!candidate) return headSha;
188
+ return gitText(projectRoot, ['merge-base', headSha, candidate], {
189
+ ...options, allowFailure: true,
190
+ }) || headSha;
191
+ }
192
+
193
+ export function captureGitSnapshot(projectRoot, { spawn = spawnSync } = {}) {
194
+ const options = { spawn };
195
+ const headSha = gitText(projectRoot, ['rev-parse', 'HEAD'], options);
196
+ const branch = gitText(projectRoot, ['symbolic-ref', '--short', '-q', 'HEAD'], {
197
+ ...options, allowFailure: true,
198
+ }) || 'HEAD';
199
+ const indexTreeSha = gitText(projectRoot, ['write-tree'], options);
200
+ const entries = changedEntries(projectRoot, options);
201
+ return {
202
+ branch,
203
+ base_sha: resolveBaseSha(projectRoot, headSha, options),
204
+ head_sha: headSha,
205
+ index_tree_sha: indexTreeSha,
206
+ worktree_digest: digestWorktreeEntries(entries),
207
+ dirty: entries.length > 0,
208
+ };
209
+ }
210
+
211
+ export function resolveEvidenceIdentity({
212
+ vaultBase,
213
+ projectRoot,
214
+ changeSlug,
215
+ sessionId = '',
216
+ context = null,
217
+ spawn = spawnSync,
218
+ } = {}) {
219
+ const project = readProjectForValidation(vaultBase);
220
+ const repository = discoverWorktreeRepository({ startDir: projectRoot, spawn });
221
+ const { registry } = readWorktreeRegistry(repository);
222
+ if (registry && project.ok && registry.projectId !== project.projectId) {
223
+ const error = new Error('PROJECT.json e registry de worktrees pertencem a projetos diferentes');
224
+ error.code = 'WENDKEEP_EVIDENCE_IDENTITY_MISMATCH';
225
+ throw error;
226
+ }
227
+ const repositoryId = context?.repositoryId
228
+ || registry?.repositoryId
229
+ || canonicalSha256({ git_common_dir: normalizedPath(repository.commonDir).toLowerCase() });
230
+ const projectId = context?.projectId
231
+ || (project.ok ? project.projectId : canonicalSha256({ repository_id: repositoryId }));
232
+ const worktreeId = context?.worktreeId || worktreeIdentity(repositoryId, repository.gitDir);
233
+ const requestedSession = String(sessionId || '').trim();
234
+ const workSessionId = context?.workSessionId
235
+ || requestedSession
236
+ || canonicalSha256({ project_id: projectId, worktree_id: worktreeId, change_slug: changeSlug });
237
+ return {
238
+ project_id: projectId,
239
+ repository_id: repositoryId,
240
+ worktree_id: worktreeId,
241
+ work_session_id: workSessionId,
242
+ };
243
+ }
244
+
245
+ export function assertStableHead(startSnapshot, finishSnapshot) {
246
+ if (startSnapshot?.head_sha !== finishSnapshot?.head_sha) {
247
+ const error = new Error(
248
+ `HEAD mudou durante verify (${startSnapshot?.head_sha || 'ausente'} -> ${finishSnapshot?.head_sha || 'ausente'}); rode novamente no commit estável`,
249
+ );
250
+ error.code = 'WENDKEEP_EVIDENCE_HEAD_CHANGED';
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ export function buildEvidenceEnvelope({
256
+ identity,
257
+ changeSlug,
258
+ snapshot,
259
+ tasksSha256,
260
+ effectiveSpecSha256,
261
+ sensorConfigSha256: configSha256,
262
+ sensors,
263
+ startedAt,
264
+ finishedAt,
265
+ version = PACKAGE_VERSION,
266
+ runtimePlatform = `${process.platform}-${process.arch}`,
267
+ } = {}) {
268
+ const envelope = {
269
+ schema_version: 2,
270
+ ...identity,
271
+ change_slug: changeSlug,
272
+ branch: snapshot.branch,
273
+ base_sha: snapshot.base_sha,
274
+ head_sha: snapshot.head_sha,
275
+ index_tree_sha: snapshot.index_tree_sha,
276
+ worktree_digest: snapshot.worktree_digest,
277
+ dirty: snapshot.dirty,
278
+ tasks_sha256: tasksSha256,
279
+ effective_spec_sha256: effectiveSpecSha256,
280
+ sensor_config_sha256: configSha256,
281
+ wendkeep_version: version,
282
+ platform: runtimePlatform,
283
+ started_at: startedAt,
284
+ finished_at: finishedAt,
285
+ sensors,
286
+ };
287
+ return { ...envelope, envelope_id: canonicalSha256(envelope) };
288
+ }
@@ -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