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.
@@ -17,6 +17,14 @@ import {
17
17
  import { parseObservabilityCheckpoint } from './session-observability-state.mjs';
18
18
  import { readObservabilityStore } from './session-observability-store.mjs';
19
19
  import { assessObservabilityFreshness } from './session-observability-lifecycle.mjs';
20
+ import { evaluateEvidenceBinding } from '../packages/vault/src/evidence-envelope.mjs';
21
+ import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
22
+ import { loadSensorsDetailed, requiredSensors } from './sensors-core.mjs';
23
+ import {
24
+ captureGitSnapshot,
25
+ resolveEvidenceIdentity,
26
+ sensorConfigSha256,
27
+ } from '../src/evidence-envelope.mjs';
20
28
 
21
29
  export function checkSessionObservability(vaultBase, deps = {}) {
22
30
  const readRegistry = deps.readRegistry || readSessionRegistry;
@@ -144,10 +152,52 @@ export function checkHarness(vaultBase, projectRoot) {
144
152
  const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
145
153
  errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
146
154
  if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
155
+ let evidence = null;
156
+ try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
157
+ if (evidence) {
158
+ const expected = {
159
+ change_slug: name,
160
+ tasks_sha256: tasksHashOf(tarefasMd),
161
+ effective_spec_sha256: `sha256:${effective.hash}`,
162
+ };
163
+ let bindingUnavailable = '';
164
+ if (evidence.schema_version === 2 && projectRoot) {
165
+ try {
166
+ const loaded = loadSensorsDetailed(projectRoot);
167
+ if (loaded.error) throw new Error(`wendkeep.sensors.json inválido: ${loaded.error}`);
168
+ expected.identity = resolveEvidenceIdentity({
169
+ vaultBase,
170
+ projectRoot,
171
+ changeSlug: name,
172
+ sessionId: evidence.work_session_id,
173
+ });
174
+ expected.snapshot = captureGitSnapshot(projectRoot);
175
+ expected.sensor_config_sha256 = sensorConfigSha256(
176
+ loaded.sensors,
177
+ requiredSensors(tasks),
178
+ );
179
+ } catch (error) {
180
+ bindingUnavailable = error.code || error.message;
181
+ }
182
+ }
183
+ const binding = evaluateEvidenceBinding(evidence, expected);
184
+ if (binding.state === 'legacy-unbound') {
185
+ attention.push(`${name}: evidence legacy-unbound — rode wendkeep verify novamente`);
186
+ } else if (binding.state !== 'bound') {
187
+ attention.push(`${name}: evidence ${binding.state} (${binding.reasons.join('; ')}) — rode wendkeep verify novamente`);
188
+ } else if (bindingUnavailable) {
189
+ attention.push(`${name}: evidence binding atual indisponível (${bindingUnavailable}) — rode doctor da raiz Git e depois wendkeep verify`);
190
+ }
191
+ }
147
192
  let verdict = null;
148
193
  try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
149
194
  if (verdict && reqIds.length) {
150
- const v = evaluateVerdict(verdict, reqIds, { tasksHash: tasksHashOf(tarefasMd), effectiveSpecHash: effective.hash });
195
+ const v = evaluateVerdict(verdict, reqIds, {
196
+ tasksHash: tasksHashOf(tarefasMd),
197
+ effectiveSpecHash: effective.hash,
198
+ evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
199
+ evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
200
+ });
151
201
  if (!v.ok) attention.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
152
202
  }
153
203
  }
@@ -5,12 +5,12 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { getLocale } from './locale.mjs';
7
7
  import {
8
- assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileSync,
8
+ assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileAtomic, writeVaultFileSync,
9
9
  } from './vault-path-safety.mjs';
10
10
 
11
- // Short stable fingerprint of tarefas.md — freshness check between package/verdict and gate.
11
+ // Canonical SHA-256 fingerprint of tarefas.md — freshness binding between package/verdict and gate.
12
12
  export function tasksHashOf(md) {
13
- return createHash('sha1').update(String(md)).digest('hex').slice(0, 12);
13
+ return `sha256:${createHash('sha256').update(String(md).replace(/\r\n?/g, '\n')).digest('hex')}`;
14
14
  }
15
15
 
16
16
  export function contentHashOf(value) {
@@ -168,7 +168,11 @@ function recordPromotedSpecs(vaultBase, capabilities) {
168
168
  return state;
169
169
  }
170
170
 
171
- export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } = {}) {
171
+ export function captureSpecBaseline(vaultBase, changeDir, {
172
+ refresh = false,
173
+ writeAtomic = writeVaultFileAtomic,
174
+ beforeRename,
175
+ } = {}) {
172
176
  const path = join(changeDir, SPEC_BASELINE_FILE);
173
177
  const checked = assertVaultPathSafe(vaultBase, path, {
174
178
  expectedType: 'file', label: 'baseline de specs da change',
@@ -177,12 +181,12 @@ export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } =
177
181
  try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
178
182
  }
179
183
  const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
180
- writeVaultFileSync(
184
+ writeAtomic(
181
185
  vaultBase,
182
186
  path,
183
187
  `${JSON.stringify(baseline, null, 2)}\n`,
184
188
  'utf8',
185
- { label: 'baseline de specs da change' },
189
+ { scopeRoot: changeDir, label: 'baseline de specs da change', beforeRename },
186
190
  );
187
191
  return baseline;
188
192
  }
@@ -474,10 +478,24 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
474
478
  // Gate check for the independent verdict (Wave A). A requirement-bearing change must have
475
479
  // a verdict that is ok and covers every declared req id. A requirement-less change passes:
476
480
  // nothing for an independent verifier to check — the sensor gate is already the proof.
477
- export function evaluateVerdict(verdict, reqIds, { tasksHash, effectiveSpecHash } = {}) {
481
+ export function evaluateVerdict(verdict, reqIds, {
482
+ tasksHash,
483
+ effectiveSpecHash,
484
+ evidenceEnvelopeId,
485
+ evidenceBinding,
486
+ } = {}) {
478
487
  const ids = reqIds || [];
479
- if (ids.length === 0) return { ok: true, missing: [] };
488
+ if (ids.length === 0 && !evidenceEnvelopeId) return { ok: true, missing: [] };
480
489
  if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
490
+ if (evidenceEnvelopeId && verdict.evidenceEnvelopeId !== evidenceEnvelopeId) {
491
+ return { ok: false, missing: [], stale: true };
492
+ }
493
+ if (evidenceBinding && (!verdict.evidenceBinding || Object.entries(evidenceBinding).some(
494
+ ([key, value]) => verdict.evidenceBinding[key] !== value,
495
+ ))) {
496
+ return { ok: false, missing: [], stale: true };
497
+ }
498
+ if (ids.length === 0) return { ok: true, missing: [] };
481
499
  // Freshness (G3/#6): a verdict minted against a different tarefas.md is stale. Verdicts
482
500
  // without a hash (pre-0.6.1) are accepted for backward compat.
483
501
  if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.76.9",
3
+ "version": "0.78.0",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
44
+ "precheck": "node --check src/worktree.mjs && node --check src/worktree-cleanup.mjs && node --check src/evidence-envelope.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs && node --check packages/vault/src/evidence-envelope.mjs",
45
45
  "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/active-context-health.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
46
46
  "test": "node --test --test-concurrency=2",
47
47
  "test:core": "node scripts/run-scope.mjs core",
@@ -249,7 +249,7 @@ async function main(argv) {
249
249
  }
250
250
  case 'worktree': {
251
251
  const { runWorktree } = await import('../../../src/worktree.mjs');
252
- process.exit(runWorktree(rest));
252
+ process.exit(await runWorktree(rest));
253
253
  break;
254
254
  }
255
255
  case 'context': {
@@ -2,6 +2,7 @@
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 { createHash } from 'node:crypto';
5
6
  import { existsSync, readFileSync } from 'node:fs';
6
7
  import { dirname, join, resolve } from 'node:path';
7
8
 
@@ -22,6 +23,22 @@ function sanitizeSensorDiagnostic(value) {
22
23
  .trim();
23
24
  }
24
25
 
26
+ function sha256(value) {
27
+ return `sha256:${createHash('sha256').update(String(value || '')).digest('hex')}`;
28
+ }
29
+
30
+ function sensorNow(now) {
31
+ const value = typeof now === 'function' ? now() : (now || new Date().toISOString());
32
+ if (value instanceof Date) return value.toISOString();
33
+ if (typeof value === 'string') return value;
34
+ return new Date(value).toISOString();
35
+ }
36
+
37
+ function elapsedMilliseconds(startedAt, finishedAt) {
38
+ const elapsed = Date.parse(finishedAt) - Date.parse(startedAt);
39
+ return Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
40
+ }
41
+
25
42
  function sensorFailureNote(result = {}) {
26
43
  const status = result.status ?? 'null';
27
44
  const header = [
@@ -86,11 +103,29 @@ export function requiredSensors(tasks) {
86
103
 
87
104
  export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
88
105
  const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
89
- const ts = now || new Date().toISOString();
90
106
  const evidence = [];
91
107
  for (const id of ids) {
108
+ const startedAt = sensorNow(now);
92
109
  const s = byId[id];
93
- if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
110
+ if (!s) {
111
+ const finishedAt = sensorNow(now);
112
+ evidence.push({
113
+ id,
114
+ status: 'red',
115
+ ts: startedAt,
116
+ severity: 'critical',
117
+ started_at: startedAt,
118
+ finished_at: finishedAt,
119
+ duration_ms: elapsedMilliseconds(startedAt, finishedAt),
120
+ exit_code: null,
121
+ command: '',
122
+ command_sha256: sha256(''),
123
+ output_sha256: sha256(''),
124
+ output_tail: '',
125
+ note: 'sensor não definido',
126
+ });
127
+ continue;
128
+ }
94
129
  const r = spawn(s.command, [], {
95
130
  cwd,
96
131
  shell: true,
@@ -99,7 +134,26 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } =
99
134
  stdio: ['ignore', 'pipe', 'pipe'],
100
135
  ...(env ? { env } : {}),
101
136
  });
102
- const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
137
+ const finishedAt = sensorNow(now);
138
+ const rawOutput = [r.stdout, r.stderr].filter(Boolean).join('\n');
139
+ const outputTail = sanitizeSensorDiagnostic(rawOutput);
140
+ const boundedOutputTail = outputTail.length > SENSOR_DIAGNOSTIC_MAX_LENGTH
141
+ ? `…${outputTail.slice(-(SENSOR_DIAGNOSTIC_MAX_LENGTH - 1))}`
142
+ : outputTail;
143
+ const entry = {
144
+ id,
145
+ status: (r.status ?? 1) === 0 ? 'green' : 'red',
146
+ ts: startedAt,
147
+ severity: s.severity || 'critical',
148
+ command: sanitizeSensorDiagnostic(s.command),
149
+ command_sha256: sha256(s.command),
150
+ started_at: startedAt,
151
+ finished_at: finishedAt,
152
+ duration_ms: elapsedMilliseconds(startedAt, finishedAt),
153
+ exit_code: Number.isInteger(r.status) ? r.status : null,
154
+ output_sha256: sha256(rawOutput),
155
+ output_tail: boundedOutputTail,
156
+ };
103
157
  if (entry.status === 'red') entry.note = sensorFailureNote(r);
104
158
  if (s.type === 'mutation' && s.report) {
105
159
  // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ function stableValue(value) {
4
+ if (Array.isArray(value)) return value.map(stableValue);
5
+ if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
6
+ return Object.fromEntries(
7
+ Object.keys(value).sort().map((key) => [key, stableValue(value[key])]),
8
+ );
9
+ }
10
+ return value;
11
+ }
12
+
13
+ export function canonicalSha256(value) {
14
+ const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array
15
+ ? value
16
+ : JSON.stringify(stableValue(value));
17
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
18
+ }
19
+
20
+ export function evidenceSensors(evidence) {
21
+ if (Array.isArray(evidence)) return evidence;
22
+ return Array.isArray(evidence?.sensors) ? evidence.sensors : [];
23
+ }
24
+
25
+ const CHECKOUT_BINDING_KEYS = [
26
+ 'project_id', 'repository_id', 'worktree_id', 'head_sha', 'index_tree_sha', 'worktree_digest',
27
+ ];
28
+
29
+ export function evidenceCheckoutBinding(evidence = {}) {
30
+ return Object.fromEntries(CHECKOUT_BINDING_KEYS.map((key) => [key, evidence[key]]));
31
+ }
32
+
33
+ export function evidenceCheckoutBindingMatches(actual, expected) {
34
+ return Boolean(actual && expected) && CHECKOUT_BINDING_KEYS.every(
35
+ (key) => actual[key] === expected[key] && expected[key] != null,
36
+ );
37
+ }
38
+
39
+ export function evaluateEvidenceBinding(evidence, expected = {}) {
40
+ if (!evidence) return { state: 'unproven', reasons: ['evidence missing'] };
41
+ if (Array.isArray(evidence) || evidence.schema_version !== 2) {
42
+ return { state: 'legacy-unbound', reasons: ['evidence schema v1 has no checkout binding'] };
43
+ }
44
+
45
+ const contextReasons = [];
46
+ if (expected.change_slug != null && evidence.change_slug !== expected.change_slug) {
47
+ contextReasons.push('change_slug mismatch');
48
+ }
49
+ for (const key of ['project_id', 'repository_id', 'worktree_id', 'work_session_id']) {
50
+ if (expected.identity?.[key] != null && evidence[key] !== expected.identity[key]) {
51
+ contextReasons.push(`${key} mismatch`);
52
+ }
53
+ }
54
+ if (contextReasons.length) return { state: 'context-mismatch', reasons: contextReasons };
55
+
56
+ const staleReasons = [];
57
+ const unsigned = { ...evidence };
58
+ delete unsigned.envelope_id;
59
+ if (!evidence.envelope_id || canonicalSha256(unsigned) !== evidence.envelope_id) {
60
+ staleReasons.push('envelope_id invalid');
61
+ }
62
+ for (const key of ['branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest', 'dirty']) {
63
+ if (expected.snapshot?.[key] != null && evidence[key] !== expected.snapshot[key]) {
64
+ staleReasons.push(`${key} changed`);
65
+ }
66
+ }
67
+ for (const key of ['tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256']) {
68
+ if (expected[key] != null && evidence[key] !== expected[key]) staleReasons.push(`${key} changed`);
69
+ }
70
+ return staleReasons.length
71
+ ? { state: 'stale', reasons: staleReasons }
72
+ : { state: 'bound', reasons: [] };
73
+ }
@@ -8,5 +8,6 @@ export * from './memory-handoff.mjs';
8
8
  export * from './memory-store.mjs';
9
9
  export * from './memory-scope.mjs';
10
10
  export * from './evidence-recall.mjs';
11
+ export * from './evidence-envelope.mjs';
11
12
  export * from './validate-core.mjs';
12
13
  export * from './validate-memory.mjs';
@@ -5,6 +5,12 @@ import { basename, join, relative } from 'node:path';
5
5
 
6
6
  import { sanitizeMemoryText } from './memory-schema.mjs';
7
7
  import { scopeForMemoryKey } from './memory-scope.mjs';
8
+ import {
9
+ evaluateEvidenceBinding,
10
+ evidenceCheckoutBinding,
11
+ evidenceCheckoutBindingMatches,
12
+ evidenceSensors,
13
+ } from './evidence-envelope.mjs';
8
14
 
9
15
  const SHARED_HANDOFF_FIELDS = Object.freeze([
10
16
  ['objective', 'objective.current'],
@@ -177,9 +183,33 @@ export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary =
177
183
  };
178
184
  }
179
185
  const sensorPath = join(archivedDir, 'evidencia.json');
180
- const sensors = readJson(sensorPath);
181
- if (Array.isArray(sensors) && sensors.length && sensors.every((item) => item?.status === 'green')) {
186
+ const sensorEnvelope = readJson(sensorPath);
187
+ const sensors = evidenceSensors(sensorEnvelope);
188
+ if (sensors.length && sensors.every((item) => item?.status === 'green')) {
182
189
  evidence.sensors = [...new Set(sensors.map((item) => String(item.id || '')).filter(Boolean))].sort();
190
+ evidence.sensors_path = vaultRel(vaultBase, sensorPath);
191
+ const assessed = evaluateEvidenceBinding(sensorEnvelope, { change_slug: slug });
192
+ evidence.sensors_binding = assessed.state;
193
+ if (sensorEnvelope?.schema_version === 2 && assessed.state === 'bound') {
194
+ const checkoutBinding = evidenceCheckoutBinding(sensorEnvelope);
195
+ const verification = readJson(join(archivedDir, 'verificacao.json'));
196
+ const crossBound = verification?.evidenceEnvelopeId === sensorEnvelope.envelope_id
197
+ && verdict?.evidenceEnvelopeId === sensorEnvelope.envelope_id
198
+ && evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)
199
+ && evidenceCheckoutBindingMatches(verdict?.evidenceBinding, checkoutBinding);
200
+ if (!crossBound) {
201
+ evidence.sensors_binding = 'stale';
202
+ evidence.sensors_binding_reasons = ['archived verification/verdict binding mismatch'];
203
+ }
204
+ } else if (assessed.reasons.length) {
205
+ evidence.sensors_binding_reasons = assessed.reasons;
206
+ }
207
+ if (sensorEnvelope?.schema_version === 2) {
208
+ evidence.sensors_envelope_id = sensorEnvelope.envelope_id;
209
+ evidence.sensors_tasks_hash = sensorEnvelope.tasks_sha256;
210
+ evidence.sensors_repository_id = sensorEnvelope.repository_id;
211
+ evidence.sensors_worktree_id = sensorEnvelope.worktree_id;
212
+ }
183
213
  }
184
214
  }
185
215
  }
@@ -279,12 +309,23 @@ export function buildSessionMemoryEvents({
279
309
  if (Array.isArray(evidence.sensors) && evidence.sensors.length) {
280
310
  events.push(makeEvent(context, {
281
311
  memoryKey: 'quality.latest-sensors',
282
- value: [...new Set(evidence.sensors.map(String))].sort(),
283
- authority: 'verified',
284
- evidence: evidence.sensors,
312
+ value: {
313
+ ids: [...new Set(evidence.sensors.map(String))].sort(),
314
+ evidence_state: evidence.sensors_binding || 'legacy-unbound',
315
+ ...(evidence.sensors_envelope_id ? { envelope_id: evidence.sensors_envelope_id } : {}),
316
+ ...(evidence.sensors_binding && !['bound', 'legacy-unbound'].includes(evidence.sensors_binding)
317
+ ? { recovery: 'reabra a change e rode wendkeep verify --deep + wk-verify' }
318
+ : {}),
319
+ },
320
+ authority: evidence.sensors_binding === 'bound' ? 'verified' : 'reported',
321
+ evidence: [evidence.sensors_path].filter(Boolean),
285
322
  scopeContext: {
286
323
  changeSlug: evidence.change?.slug || normalizedShared?.change_slug,
287
324
  tasksHash: evidence.sensors_tasks_hash || normalizedShared?.tasks_hash,
325
+ repositoryId: evidence.sensors_repository_id,
326
+ worktreeId: evidence.sensors_worktree_id,
327
+ evidenceEnvelopeId: evidence.sensors_envelope_id,
328
+ evidenceState: evidence.sensors_binding,
288
329
  },
289
330
  }));
290
331
  }
@@ -225,10 +225,20 @@ export function writeVaultFileSync(vaultBase, targetPath, content, encoding = 'u
225
225
  export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding = 'utf8', {
226
226
  label = 'arquivo atômico do Vault',
227
227
  code = 'VAULT_PATH_UNSAFE',
228
+ scopeRoot = '',
229
+ beforeRename,
228
230
  } = {}) {
229
231
  const checked = assertVaultPathSafe(vaultBase, targetPath, {
230
232
  expectedType: 'file', label, code,
231
233
  });
234
+ if (scopeRoot) {
235
+ const scope = assertVaultPathSafe(vaultBase, scopeRoot, {
236
+ allowMissing: false, expectedType: 'directory', label: `escopo de ${label}`, code,
237
+ });
238
+ if (!containedBy(scope.target, checked.target)) {
239
+ throw unsafe(`${label} escapa do escopo autorizado: ${checked.target}`, code);
240
+ }
241
+ }
232
242
  assertVaultPathSafe(vaultBase, dirname(checked.target), {
233
243
  allowMissing: false, expectedType: 'directory', label: `ancestral de ${label}`, code,
234
244
  });
@@ -245,6 +255,7 @@ export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding =
245
255
  allowMissing: false, expectedType: 'file', label: `temporário de ${label}`, code,
246
256
  });
247
257
  assertVaultPathSafe(vaultBase, checked.target, { expectedType: 'file', label, code });
258
+ if (typeof beforeRename === 'function') beforeRename({ temporary: tmp, target: checked.target });
248
259
  renameSync(tmp, checked.target);
249
260
  tmpCreated = false;
250
261
  assertVaultPathSafe(vaultBase, checked.target, {
@@ -0,0 +1,92 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/rogersialves/wendkeep/schema/wendkeep.evidence-envelope-v2.schema.json",
4
+ "title": "WendKeep Evidence Envelope v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "project_id",
10
+ "repository_id",
11
+ "worktree_id",
12
+ "work_session_id",
13
+ "change_slug",
14
+ "branch",
15
+ "base_sha",
16
+ "head_sha",
17
+ "index_tree_sha",
18
+ "worktree_digest",
19
+ "dirty",
20
+ "tasks_sha256",
21
+ "effective_spec_sha256",
22
+ "sensor_config_sha256",
23
+ "wendkeep_version",
24
+ "platform",
25
+ "started_at",
26
+ "finished_at",
27
+ "sensors",
28
+ "envelope_id"
29
+ ],
30
+ "properties": {
31
+ "schema_version": { "const": 2 },
32
+ "project_id": { "type": "string", "minLength": 1 },
33
+ "repository_id": { "type": "string", "minLength": 1 },
34
+ "worktree_id": { "type": "string", "minLength": 1 },
35
+ "work_session_id": { "type": "string", "minLength": 1 },
36
+ "change_slug": { "type": "string", "minLength": 1 },
37
+ "branch": { "type": "string", "minLength": 1 },
38
+ "base_sha": { "$ref": "#/$defs/gitObject" },
39
+ "head_sha": { "$ref": "#/$defs/gitObject" },
40
+ "index_tree_sha": { "$ref": "#/$defs/gitObject" },
41
+ "worktree_digest": { "$ref": "#/$defs/sha256" },
42
+ "dirty": { "type": "boolean" },
43
+ "tasks_sha256": { "$ref": "#/$defs/sha256" },
44
+ "effective_spec_sha256": { "$ref": "#/$defs/sha256" },
45
+ "sensor_config_sha256": { "$ref": "#/$defs/sha256" },
46
+ "wendkeep_version": { "type": "string", "minLength": 1 },
47
+ "platform": { "type": "string", "minLength": 1 },
48
+ "started_at": { "type": "string", "format": "date-time" },
49
+ "finished_at": { "type": "string", "format": "date-time" },
50
+ "envelope_id": { "$ref": "#/$defs/sha256" },
51
+ "sensors": {
52
+ "type": "array",
53
+ "items": { "$ref": "#/$defs/sensor" }
54
+ }
55
+ },
56
+ "$defs": {
57
+ "gitObject": { "type": "string", "pattern": "^[a-f0-9]{40,64}$" },
58
+ "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
59
+ "sensor": {
60
+ "type": "object",
61
+ "required": [
62
+ "id",
63
+ "status",
64
+ "severity",
65
+ "command",
66
+ "command_sha256",
67
+ "started_at",
68
+ "finished_at",
69
+ "duration_ms",
70
+ "exit_code",
71
+ "output_sha256",
72
+ "output_tail"
73
+ ],
74
+ "properties": {
75
+ "id": { "type": "string", "minLength": 1 },
76
+ "status": { "enum": ["green", "red"] },
77
+ "severity": { "enum": ["critical", "warning"] },
78
+ "command": { "type": "string" },
79
+ "command_sha256": { "$ref": "#/$defs/sha256" },
80
+ "started_at": { "type": "string", "format": "date-time" },
81
+ "finished_at": { "type": "string", "format": "date-time" },
82
+ "duration_ms": { "type": "number", "minimum": 0 },
83
+ "exit_code": { "type": ["integer", "null"] },
84
+ "output_sha256": { "$ref": "#/$defs/sha256" },
85
+ "output_tail": { "type": "string", "maxLength": 2000 },
86
+ "note": { "type": "string", "maxLength": 2000 },
87
+ "survivors": { "type": "array" },
88
+ "ts": { "type": "string", "format": "date-time" }
89
+ }
90
+ }
91
+ }
92
+ }