wendkeep 0.85.1 → 0.87.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.
Files changed (49) hide show
  1. package/.githooks/commit-msg +16 -0
  2. package/.githooks/prepare-commit-msg +16 -0
  3. package/CHANGELOG.md +40 -0
  4. package/README.en.md +4 -1
  5. package/README.md +4 -1
  6. package/docs/en/commands/commit.md +159 -0
  7. package/docs/en/commands/evidence-embeddings.md +243 -0
  8. package/docs/en/commands/mcp.md +67 -7
  9. package/docs/pt-BR/commands/commit.md +159 -0
  10. package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
  11. package/docs/pt-BR/commands/mcp.md +66 -7
  12. package/hooks/evidence-context.mjs +41 -7
  13. package/hooks/evidence-recall.mjs +10 -0
  14. package/package.json +5 -2
  15. package/packages/cli/src/index.mjs +11 -1
  16. package/packages/commit/package.json +6 -0
  17. package/packages/commit/src/cli.mjs +89 -0
  18. package/packages/commit/src/commit-input.mjs +181 -0
  19. package/packages/commit/src/commit-message.mjs +51 -0
  20. package/packages/commit/src/commit-policy.mjs +144 -0
  21. package/packages/commit/src/git-runtime.mjs +428 -0
  22. package/packages/commit/src/index.mjs +28 -0
  23. package/packages/commit/src/proof-validation.mjs +443 -0
  24. package/packages/mcp/src/effects.mjs +3 -2
  25. package/packages/mcp/src/evidence-recall.mjs +130 -0
  26. package/packages/mcp/src/executor.mjs +4 -0
  27. package/packages/mcp/src/server.mjs +31 -1
  28. package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
  29. package/packages/vault/src/evidence-index-store.mjs +360 -0
  30. package/packages/vault/src/evidence-recall-page.mjs +381 -0
  31. package/packages/vault/src/evidence-search-index.mjs +917 -0
  32. package/packages/vault/src/index.mjs +12 -1
  33. package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
  34. package/packages/vault/src/memory-ledger-view.mjs +41 -0
  35. package/packages/vault/src/memory-rotation-store.mjs +967 -0
  36. package/packages/vault/src/memory-segment-store.mjs +820 -0
  37. package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
  38. package/packages/vault/src/memory-store-base.mjs +1161 -0
  39. package/packages/vault/src/memory-store-core.mjs +2 -0
  40. package/packages/vault/src/memory-store.mjs +46 -1161
  41. package/schema/commit-message-v1.schema.json +75 -0
  42. package/scripts/validate-commit-range.mjs +244 -0
  43. package/src/doctor.mjs +48 -5
  44. package/src/evidence-search-health.mjs +221 -0
  45. package/src/git-commit-hooks.mjs +112 -0
  46. package/src/init.mjs +13 -0
  47. package/src/memory-scale-health.mjs +210 -0
  48. package/src/observer-snapshot.mjs +87 -1
  49. package/src/skills-seed.mjs +79 -0
@@ -0,0 +1,443 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { basename, isAbsolute } from 'node:path';
3
+
4
+ import { parseTasks } from '../../../hooks/change-core.mjs';
5
+ import { evaluateVerdict, tasksHashOf } from '../../../hooks/spec-core.mjs';
6
+ import { deriveTaskContracts, evaluateTaskContracts } from '../../../src/task-contracts.mjs';
7
+ import { evaluateTddAttestation } from '../../../src/tdd-attestation.mjs';
8
+ import { sensorConfigSha256 } from '../../../src/evidence-envelope.mjs';
9
+ import { classifyReceipt } from '../../../src/provenance-gate.mjs';
10
+ import { verifyReceiptChain } from '../../../src/receipt-ledger.mjs';
11
+ import { requiredSensors, runSensors } from '../../harness/src/sensors-core.mjs';
12
+ import {
13
+ canonicalSha256,
14
+ evaluateEvidenceBinding,
15
+ evidenceSensors,
16
+ } from '../../vault/src/evidence-envelope.mjs';
17
+
18
+ const SHA256 = /^[a-f0-9]{64}$/;
19
+ const SIGNED_REF = /^(.*)@sha256:([a-f0-9]{64})$/;
20
+ const SENSOR_HASH = /^sha256:[a-f0-9]{64}$/;
21
+ const GIT_OBJECT = /^[a-f0-9]{40,64}$/;
22
+ const ENVELOPE_REQUIRED = [
23
+ 'schema_version', 'project_id', 'repository_id', 'worktree_id', 'work_session_id',
24
+ 'change_slug', 'branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest',
25
+ 'dirty', 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256',
26
+ 'wendkeep_version', 'platform', 'started_at', 'finished_at', 'sensors', 'envelope_id',
27
+ ];
28
+ const ENVELOPE_ALLOWED = new Set([...ENVELOPE_REQUIRED, 'host_coverage', 'tdd_attestations']);
29
+ const PROOF_BINDING_KEYS = [
30
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
31
+ 'branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest', 'dirty',
32
+ 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256',
33
+ ];
34
+ const EXPECTED_CONTEXT_FIELDS = [
35
+ 'projectId', 'repositoryId', 'worktreeId', 'workSessionId', 'changeSlug',
36
+ 'branch', 'baseSha', 'headSha', 'indexTreeSha', 'worktreeDigest', 'dirty',
37
+ 'tasksSha256', 'effectiveSpecSha256', 'sensorConfigSha256',
38
+ ];
39
+ const SENSOR_PROOF = Symbol('wendkeep.commit.sensor-proof');
40
+ const REMOTE_EVIDENCE_KINDS = new Set(['adr', 'design', 'spec', 'task']);
41
+ const SENSOR_RESULT_BINDING_FIELDS = [
42
+ 'id', 'status', 'severity', 'command', 'command_sha256', 'exit_code',
43
+ 'output_sha256', 'output_tail',
44
+ ];
45
+
46
+ function fail(code, message) {
47
+ throw Object.assign(new Error(message), { code });
48
+ }
49
+
50
+ export function contentSha256(content) {
51
+ return createHash('sha256').update(String(content ?? ''), 'utf8').digest('hex');
52
+ }
53
+
54
+ export function signedEvidenceRef(path, content) {
55
+ return `${path}@sha256:${contentSha256(content)}`;
56
+ }
57
+
58
+ export function parseSignedEvidenceRef(value) {
59
+ const match = String(value || '').match(SIGNED_REF);
60
+ if (!match) fail('WENDKEEP_COMMIT_EVIDENCE_DIGEST_MISSING', 'evidence reference must carry a SHA-256 content digest');
61
+ const path = match[1].replaceAll('\\', '/');
62
+ if (!path || isAbsolute(path) || path.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
63
+ fail('WENDKEEP_COMMIT_REFERENCE_INVALID', 'signed evidence must use a canonical repository-relative path');
64
+ }
65
+ return { path, sha256: match[2] };
66
+ }
67
+
68
+ export function commitTaskSensorIds(entries = []) {
69
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
70
+ if (taskEntries.length !== 1) return [];
71
+ return requiredSensors(parseTasks(taskEntries[0].content));
72
+ }
73
+
74
+ export function commitTaskRequirementIds(entries = []) {
75
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
76
+ if (taskEntries.length !== 1) return [];
77
+ return [...new Set(parseTasks(taskEntries[0].content).flatMap((task) => task.reqs || []))];
78
+ }
79
+
80
+ export function collectCommitSensorProof({ sensors = [], ids = [], cwd, env } = {}) {
81
+ const selected = [...new Set(ids.map((id) => String(id || '').trim()).filter(Boolean))];
82
+ const definitions = new Map(sensors.map((sensor) => [String(sensor?.id || ''), sensor]));
83
+ const missing = selected.filter((id) => !definitions.has(id));
84
+ if (missing.length) {
85
+ fail('WENDKEEP_COMMIT_SENSOR_CONFIG_MISSING', `required commit sensor is not configured: ${missing.join(', ')}`);
86
+ }
87
+ const results = runSensors(sensors, selected, { cwd, env });
88
+ return Object.freeze({
89
+ [SENSOR_PROOF]: true,
90
+ ids: selected,
91
+ definitions: selected.map((id) => definitions.get(id)),
92
+ results,
93
+ configSha256: sensorConfigSha256(sensors, selected),
94
+ });
95
+ }
96
+
97
+ function json(content, path) {
98
+ try { return JSON.parse(content); }
99
+ catch { fail('WENDKEEP_COMMIT_EVIDENCE_INVALID', `structured evidence is invalid JSON: ${path}`); }
100
+ }
101
+
102
+ function validateSensor(sensor, path) {
103
+ const required = [
104
+ 'id', 'status', 'severity', 'command', 'command_sha256', 'started_at', 'finished_at',
105
+ 'duration_ms', 'exit_code', 'output_sha256', 'output_tail',
106
+ ];
107
+ if (!sensor || required.some((field) => sensor[field] === undefined)) {
108
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `sensor provenance is incomplete: ${path}`);
109
+ }
110
+ if (!sensor.id || sensor.status !== 'green' || !['critical', 'warning'].includes(sensor.severity) || sensor.exit_code !== 0
111
+ || !sensor.command || sensor.command_sha256 !== `sha256:${contentSha256(sensor.command)}`
112
+ || !SENSOR_HASH.test(sensor.output_sha256)
113
+ || !Number.isFinite(sensor.duration_ms) || sensor.duration_ms < 0
114
+ || typeof sensor.output_tail !== 'string' || sensor.output_tail.length > 2_000
115
+ || Number.isNaN(Date.parse(sensor.started_at)) || Number.isNaN(Date.parse(sensor.finished_at))) {
116
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `sensor result is not a complete green observation: ${path}`);
117
+ }
118
+ }
119
+
120
+ function executionSensors(task, proof) {
121
+ const ids = requiredSensors(task.tasks);
122
+ if (!ids.length) return [];
123
+ if (!proof?.[SENSOR_PROOF]) {
124
+ fail('WENDKEEP_COMMIT_TESTS_UNPROVEN', 'required sensors were not executed by the canonical collector');
125
+ }
126
+ if (JSON.stringify([...proof.ids].sort()) !== JSON.stringify([...ids].sort())) {
127
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'executed sensors do not match canonical task requirements');
128
+ }
129
+ const definitions = new Map(proof.definitions.map((sensor) => [String(sensor?.id || ''), sensor]));
130
+ for (const result of proof.results) {
131
+ validateSensor(result, `sensor:${result.id}`);
132
+ const definition = definitions.get(result.id);
133
+ if (!definition || result.command !== definition.command
134
+ || result.severity !== (definition.severity || 'critical')) {
135
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', `sensor result does not match configured command: ${result.id}`);
136
+ }
137
+ }
138
+ if (proof.results.length !== ids.length) {
139
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'canonical sensor result set is incomplete');
140
+ }
141
+ return proof.results;
142
+ }
143
+
144
+ function assertEnvelopeSensorsMatchExecution(envelopeSensors, collectedSensors) {
145
+ if (!envelopeSensors.length) return;
146
+ if (!collectedSensors.length) {
147
+ fail('WENDKEEP_COMMIT_SENSOR_PROOF_UNAUTHENTICATED', 'Envelope sensors require canonical reexecution');
148
+ }
149
+ const envelopeIds = envelopeSensors.map((sensor) => sensor.id);
150
+ const executionIds = collectedSensors.map((sensor) => sensor.id);
151
+ if (new Set(envelopeIds).size !== envelopeIds.length
152
+ || JSON.stringify([...envelopeIds].sort()) !== JSON.stringify([...executionIds].sort())) {
153
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'Envelope sensors do not exactly match reexecuted task sensors');
154
+ }
155
+ const observed = new Map(collectedSensors.map((sensor) => [sensor.id, sensor]));
156
+ for (const sensor of envelopeSensors) {
157
+ const actual = observed.get(sensor.id);
158
+ if (!actual || SENSOR_RESULT_BINDING_FIELDS.some((field) => sensor[field] !== actual[field])) {
159
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', `Envelope sensor does not match canonical reexecution: ${sensor.id}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ function validateAttestation(attestation, path) {
165
+ const identity = Object.fromEntries([
166
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
167
+ ].map((field) => [field, String(attestation?.[field] || '').trim()]));
168
+ const causalSeal = {
169
+ ...identity,
170
+ task_id: String(attestation?.task_id || '').trim(),
171
+ requirement_id: String(attestation?.requirement_id || '').trim(),
172
+ };
173
+ if (attestation?.schema_version !== 1 || !SHA256.test(String(attestation.attestation_id || ''))
174
+ || Object.values(causalSeal).some((value) => !value)
175
+ || attestation.attestation_id !== canonicalSha256(causalSeal).replace(/^sha256:/, '')
176
+ || !['red-observed', 'green-observed', 'invalid', 'waived'].includes(attestation.state)
177
+ || !Array.isArray(attestation.test_paths)
178
+ || attestation.test_paths.some((testPath) => typeof testPath !== 'string' || !testPath.trim())) {
179
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `TDD attestation is invalid: ${path}`);
180
+ }
181
+ }
182
+
183
+ function validateEnvelopeShape(payload, path) {
184
+ const missing = ENVELOPE_REQUIRED.filter((field) => payload?.[field] === undefined);
185
+ const unknown = Object.keys(payload || {}).filter((field) => !ENVELOPE_ALLOWED.has(field));
186
+ const invalidText = [
187
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
188
+ 'branch', 'wendkeep_version', 'platform',
189
+ ].some((field) => typeof payload?.[field] !== 'string' || !payload[field]);
190
+ const invalidGit = ['base_sha', 'head_sha', 'index_tree_sha']
191
+ .some((field) => !GIT_OBJECT.test(String(payload?.[field] || '')));
192
+ const invalidHash = ['worktree_digest', 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256', 'envelope_id']
193
+ .some((field) => !SENSOR_HASH.test(String(payload?.[field] || '')));
194
+ const invalidDates = ['started_at', 'finished_at']
195
+ .some((field) => typeof payload?.[field] !== 'string' || Number.isNaN(Date.parse(payload[field])));
196
+ if (missing.length || unknown.length || invalidText || invalidGit || invalidHash || invalidDates
197
+ || typeof payload?.dirty !== 'boolean' || !Array.isArray(payload?.sensors)
198
+ || (payload.tdd_attestations !== undefined && !Array.isArray(payload.tdd_attestations))) {
199
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope schema/binding is incomplete: ${path}`);
200
+ }
201
+ }
202
+
203
+ function validateEnvelope(payload, entry, context) {
204
+ if (payload?.schema_version !== 2) {
205
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope v2 is required: ${entry.path}`);
206
+ }
207
+ const sensors = evidenceSensors(payload);
208
+ if (!sensors.length) fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope has no sensors: ${entry.path}`);
209
+ validateEnvelopeShape(payload, entry.path);
210
+ const missingExpected = EXPECTED_CONTEXT_FIELDS.filter((field) => (
211
+ context[field] === undefined || context[field] === null || context[field] === ''
212
+ ));
213
+ if (missingExpected.length) {
214
+ fail(
215
+ 'WENDKEEP_COMMIT_BINDING_INCOMPLETE',
216
+ `canonical expected binding is incomplete (${missingExpected.join(', ')}): ${entry.path}`,
217
+ );
218
+ }
219
+ const expected = {
220
+ change_slug: context.changeSlug,
221
+ identity: {
222
+ project_id: context.projectId,
223
+ repository_id: context.repositoryId,
224
+ worktree_id: context.worktreeId,
225
+ work_session_id: context.workSessionId,
226
+ },
227
+ snapshot: {
228
+ branch: context.branch,
229
+ base_sha: context.baseSha,
230
+ head_sha: context.headSha,
231
+ index_tree_sha: context.indexTreeSha,
232
+ worktree_digest: context.worktreeDigest,
233
+ dirty: context.dirty,
234
+ },
235
+ tasks_sha256: context.tasksSha256,
236
+ effective_spec_sha256: context.effectiveSpecSha256,
237
+ sensor_config_sha256: context.sensorConfigSha256,
238
+ };
239
+ const assessment = evaluateEvidenceBinding(payload, expected);
240
+ if (assessment.state !== 'bound') {
241
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `Evidence Envelope is not internally bound: ${entry.path}`);
242
+ }
243
+ for (const sensor of sensors) validateSensor(sensor, entry.path);
244
+ const attestations = (payload.tdd_attestations || []).map((attestation) => {
245
+ validateAttestation(attestation, entry.path);
246
+ return evaluateTddAttestation(attestation, {
247
+ branch: payload.branch,
248
+ head_sha: payload.head_sha,
249
+ index_tree_sha: payload.index_tree_sha,
250
+ worktree_digest: payload.worktree_digest,
251
+ }, { mutationSurvivors: sensors.flatMap((sensor) => sensor.survivors || []) });
252
+ });
253
+ if (attestations.some((attestation) => !['green-observed', 'waived'].includes(attestation.state))) {
254
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `TDD attestation is stale or invalid: ${entry.path}`);
255
+ }
256
+ return { payload, sensors, attestations };
257
+ }
258
+
259
+ function validateAuthority(entry, authority) {
260
+ const normalized = entry.content.replace(/\r\n?/g, '\n');
261
+ if (entry.kind === 'adr') {
262
+ const id = authority?.adr || '';
263
+ if (authority?.kind !== 'adr' || entry.path !== authority.ref
264
+ || !basename(entry.path).toUpperCase().includes(id)
265
+ || !new RegExp(`^(?:#{1,6}\\s+|id:\\s*["']?)${id}\\b`, 'mi').test(normalized)
266
+ || (authority.issue && !new RegExp(`(^|\\s)${authority.issue.replace('#', '#\\s*')}\\b`, 'm').test(normalized))) {
267
+ fail('WENDKEEP_COMMIT_AUTHORITY_MISMATCH', 'ADR artifact does not match its causal ID, path and issue');
268
+ }
269
+ }
270
+ if (entry.kind === 'design') {
271
+ const issue = authority?.issue || '';
272
+ const issueNumber = issue.replace('#', '');
273
+ if (authority?.kind !== 'native' || entry.path !== authority.design
274
+ || !new RegExp(`^#{1,6}\\s+(?:.*\\s)?#${issueNumber}(?:\\s|\\b|—|-)`, 'mi').test(normalized)) {
275
+ fail('WENDKEEP_COMMIT_AUTHORITY_MISMATCH', 'design artifact does not own the declared issue at the canonical path');
276
+ }
277
+ }
278
+ }
279
+
280
+ function requirePublicAuthority(entries, authority) {
281
+ const kind = authority?.kind === 'adr' ? 'adr' : 'design';
282
+ const matches = entries.filter((entry) => entry.kind === kind);
283
+ if (matches.length !== 1) {
284
+ fail('WENDKEEP_COMMIT_REMOTE_AUTHORITY_MISSING', `exactly one versioned ${kind} authority artifact is required`);
285
+ }
286
+ }
287
+
288
+ function taskFacts(entries, context, envelope, collectedSensors = []) {
289
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
290
+ if (taskEntries.length !== 1) fail('WENDKEEP_COMMIT_EVIDENCE_INCOMPLETE', 'exactly one canonical task artifact is required');
291
+ const source = taskEntries[0].content;
292
+ const tasks = parseTasks(source);
293
+ if (!tasks.length) fail('WENDKEEP_COMMIT_TASKS_INVALID', 'canonical task artifact has no typed checklist tasks');
294
+ if (new Set(tasks.map((task) => task.id)).size !== tasks.length) {
295
+ fail('WENDKEEP_COMMIT_TASKS_INVALID', 'canonical task IDs must be unique');
296
+ }
297
+ const contracts = deriveTaskContracts({
298
+ projectId: envelope?.payload.project_id || context.projectId || 'commit-project',
299
+ changeSlug: envelope?.payload.change_slug || context.changeSlug || 'commit-authority',
300
+ tasks,
301
+ profile: context.profile || 'OFF',
302
+ activeContextId: context.activeContextId || envelope?.payload.work_session_id || context.stagedHash,
303
+ headSha: envelope?.payload.head_sha || context.headSha || context.stagedHash,
304
+ tasksSha256: tasksHashOf(source),
305
+ effectiveSpecSha256: envelope?.payload.effective_spec_sha256 || context.effectiveSpecSha256 || canonicalSha256([]),
306
+ artifactManifestSha256: canonicalSha256([]),
307
+ evidenceEnvelopeId: envelope?.payload.envelope_id || null,
308
+ tddAttestations: envelope?.attestations || [],
309
+ });
310
+ const evaluations = evaluateTaskContracts({
311
+ contracts,
312
+ binding: contracts[0]?.binding || {},
313
+ requirement_ids: [...new Set(tasks.flatMap((task) => task.reqs || []))],
314
+ sensor_results: [...(envelope?.sensors || []), ...collectedSensors],
315
+ artifact_results: [],
316
+ });
317
+ if (!evaluations.length || evaluations.some((result) => !result.can_complete)) {
318
+ fail('WENDKEEP_COMMIT_TASKS_INCOMPLETE', 'canonical Task Contracts are not completed');
319
+ }
320
+ const renderedTasks = tasks.map((task) => `${task.id}: ${task.text}`);
321
+ return { source, tasks, renderedTasks, tasksHash: tasksHashOf(source) };
322
+ }
323
+
324
+ function publicSpecFacts(entries, tasks) {
325
+ const specs = entries.filter((entry) => entry.kind === 'spec');
326
+ const requirements = [...new Set(tasks.flatMap((task) => task.reqs || []))];
327
+ if (requirements.length && !specs.length) {
328
+ fail('WENDKEEP_COMMIT_REMOTE_SPEC_MISSING', 'requirements require a versioned sanitized public spec');
329
+ }
330
+ for (const requirement of requirements) {
331
+ const escaped = requirement.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
332
+ const heading = new RegExp(`^#{1,6}\\s+(?:Requisito|Requirement):\\s*${escaped}(?:\\s|—|-|$)`, 'mi');
333
+ const jsonId = new RegExp(`"(?:id|requirement_id)"\\s*:\\s*"${escaped}"`);
334
+ if (!specs.some((entry) => heading.test(entry.content) || jsonId.test(entry.content))) {
335
+ fail('WENDKEEP_COMMIT_REMOTE_SPEC_INCOMPLETE', `public spec does not define requirement ${requirement}`);
336
+ }
337
+ }
338
+ return specs;
339
+ }
340
+
341
+ function validateVerdict(payload, entry, task, envelope) {
342
+ if (!envelope) fail('WENDKEEP_COMMIT_EVIDENCE_INCOMPLETE', 'verdict requires its Evidence Envelope v2');
343
+ if (!payload?.author_session_id || !payload?.verifier_session_id
344
+ || payload.author_session_id === payload.verifier_session_id) {
345
+ fail('WENDKEEP_COMMIT_VERDICT_NOT_INDEPENDENT', `verdict lacks independent author/verifier identities: ${entry.path}`);
346
+ }
347
+ const reqIds = [...new Set(task.tasks.flatMap((item) => item.reqs || []))];
348
+ const expectedBinding = Object.fromEntries(PROOF_BINDING_KEYS.map((key) => [key, envelope.payload[key]]));
349
+ if (payload.tasksHash !== task.tasksHash
350
+ || payload.effectiveSpecHash !== envelope.payload.effective_spec_sha256.replace(/^sha256:/, '')
351
+ || payload.evidenceEnvelopeId !== envelope.payload.envelope_id
352
+ || !payload.evidenceBinding
353
+ || PROOF_BINDING_KEYS.some((key) => payload.evidenceBinding[key] !== expectedBinding[key])
354
+ || !Array.isArray(payload.coverage)
355
+ || payload.coverage.some((item) => !item || typeof item.req !== 'string'
356
+ || item.covered !== true || typeof item.evidence !== 'string' || !item.evidence.trim())) {
357
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `verdict lacks complete canonical seals or coverage: ${entry.path}`);
358
+ }
359
+ const verdict = evaluateVerdict(payload, reqIds, {
360
+ tasksHash: task.tasksHash,
361
+ effectiveSpecHash: envelope.payload.effective_spec_sha256?.replace(/^sha256:/, ''),
362
+ evidenceEnvelopeId: envelope.payload.envelope_id,
363
+ evidenceBinding: expectedBinding,
364
+ });
365
+ if (!verdict.ok || verdict.stale || verdict.missing?.length) {
366
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `verdict is stale or has incomplete coverage: ${entry.path}`);
367
+ }
368
+ }
369
+
370
+ function validateReceipt(payload, entry, stagedHash) {
371
+ if (!Array.isArray(payload?.records) || !payload.records.length) {
372
+ fail('WENDKEEP_COMMIT_RECEIPT_INVALID', `receipt bundle has no signed chain: ${entry.path}`);
373
+ }
374
+ verifyReceiptChain({ records: payload.records, checkpoint: payload.checkpoint ?? null });
375
+ const receipt = payload.records.at(-1);
376
+ const subject = payload.subject || receipt.subject || {};
377
+ if (subject.staged_diff_sha256 !== stagedHash) {
378
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `receipt is not bound to the staged diff: ${entry.path}`);
379
+ }
380
+ const result = classifyReceipt({ receipt: { ...receipt, ...receipt.subject }, observation: payload.observation, subject });
381
+ if (result.state !== 'verified') {
382
+ fail('WENDKEEP_COMMIT_RECEIPT_INVALID', `receipt schema, chain, observation or status is not verified: ${entry.path}`);
383
+ }
384
+ }
385
+
386
+ export function validateCommitProofSet({ entries, authority, stagedHash, context = {} } = {}) {
387
+ if (!SHA256.test(String(stagedHash || ''))) fail('WENDKEEP_COMMIT_STALE_INPUT', 'staged diff hash is invalid');
388
+ requirePublicAuthority(entries || [], authority);
389
+ for (const entry of entries || []) {
390
+ if (contentSha256(entry.content) !== entry.sha256) {
391
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `evidence digest mismatch: ${entry.path}`);
392
+ }
393
+ if (!entry.content.trim()) fail('WENDKEEP_COMMIT_EVIDENCE_EMPTY', `evidence is empty: ${entry.path}`);
394
+ if (['adr', 'design'].includes(entry.kind)) validateAuthority(entry, authority);
395
+ }
396
+ const structured = (entries || []).map((entry) => (
397
+ ['evidence', 'receipt', 'verdict'].includes(entry.kind) ? { entry, payload: json(entry.content, entry.path) } : null
398
+ )).filter(Boolean);
399
+ const taskEntry = (entries || []).find((entry) => entry.kind === 'task');
400
+ const preliminaryTasks = taskEntry ? parseTasks(taskEntry.content) : [];
401
+ const expectedContext = taskEntry
402
+ ? { ...context, tasksSha256: tasksHashOf(taskEntry.content) }
403
+ : context;
404
+ const hasVerdict = structured.some(({ entry }) => entry.kind === 'verdict');
405
+ const envelopes = structured.filter(({ entry }) => entry.kind === 'evidence')
406
+ .map(({ entry, payload }) => validateEnvelope(payload, entry, expectedContext));
407
+ if (envelopes.length > 1) fail('WENDKEEP_COMMIT_EVIDENCE_AMBIGUOUS', 'only one Evidence Envelope is allowed');
408
+ const sensors = envelopes[0]?.sensors || [];
409
+ const authenticatedAttestation = envelopes[0]?.attestations
410
+ ?.some((attestation) => attestation.state === 'green-observed') || false;
411
+ if (envelopes[0] && !hasVerdict && !authenticatedAttestation) {
412
+ fail(
413
+ 'WENDKEEP_COMMIT_SENSOR_PROOF_UNAUTHENTICATED',
414
+ 'Evidence Envelope sensor record requires an independently bound verdict or attestation',
415
+ );
416
+ }
417
+ const collectedSensors = executionSensors({ tasks: preliminaryTasks }, context.executionProof);
418
+ assertEnvelopeSensorsMatchExecution(sensors, collectedSensors);
419
+ const task = taskFacts(entries || [], { ...expectedContext, stagedHash }, envelopes[0], collectedSensors);
420
+ publicSpecFacts(entries || [], task.tasks);
421
+ if (envelopes[0] && envelopes[0].payload.tasks_sha256 !== task.tasksHash) {
422
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', 'Evidence Envelope tasks_sha256 does not match the canonical task artifact');
423
+ }
424
+ for (const { entry, payload } of structured) {
425
+ if (entry.kind === 'verdict') validateVerdict(payload, entry, task, envelopes[0]);
426
+ if (entry.kind === 'receipt') validateReceipt(payload, entry, stagedHash);
427
+ }
428
+ const tests = [
429
+ ...collectedSensors.map((sensor) => `sensor:${sensor.id} (${sensor.command})`),
430
+ ];
431
+ if (!tests.length) {
432
+ fail('WENDKEEP_COMMIT_TESTS_UNPROVEN', 'no authenticated execution proof supplies tests');
433
+ }
434
+ return {
435
+ evidence: (entries || []).filter((entry) => REMOTE_EVIDENCE_KINDS.has(entry.kind)).map((entry) => ({
436
+ kind: entry.kind,
437
+ ref: signedEvidenceRef(entry.path, entry.content),
438
+ status: 'verified',
439
+ })),
440
+ tasks: task.renderedTasks,
441
+ tests: [...new Set(tests)].sort((left, right) => left.localeCompare(right, 'en')),
442
+ };
443
+ }
@@ -6,6 +6,7 @@ const TOOLS = [
6
6
  ['wendkeep_project_status', 'read', 'project:status'],
7
7
  ['wendkeep_context_status', 'read', 'context:status'],
8
8
  ['wendkeep_memory_recall', 'read', 'memory:recall'],
9
+ ['wendkeep_evidence_recall', 'read', 'evidence:recall'],
9
10
  ['wendkeep_memory_conflicts', 'read', 'memory:conflicts'],
10
11
  ['wendkeep_change_list', 'read', 'change:list'],
11
12
  ['wendkeep_change_show', 'read', 'change:show'],
@@ -55,10 +56,10 @@ function deepFreeze(value) {
55
56
 
56
57
  export const MCP_EFFECT_MANIFEST = deepFreeze({
57
58
  schema_version: 1,
58
- catalog_version: '2026-08-24',
59
+ catalog_version: '2026-08-27',
59
60
  server_aliases: ['wendkeep', 'wendkeep-native'],
60
61
  tools: TOOLS,
61
- integrity: 'sha256:de96c2f3f5ae3d814440b541db778f87be4550ebd2ce9dd5ae2377f354770046',
62
+ integrity: 'sha256:1bb4d5dc62008ed23afa0d850a89bbbb77ea9cc31da79cde557aa2f02034fdde',
62
63
  });
63
64
 
64
65
  export function verifyMcpEffectManifest(manifest) {
@@ -0,0 +1,130 @@
1
+ import {
2
+ EVIDENCE_RECALL_DEFAULT_LIMIT,
3
+ EVIDENCE_RECALL_DEFAULT_MAX_BYTES,
4
+ EVIDENCE_RECALL_MAX_LIMIT,
5
+ EvidenceRecallBudgetError,
6
+ EvidenceRecallCursorError,
7
+ recallEvidencePage,
8
+ } from '../../vault/src/evidence-recall-page.mjs';
9
+ import {
10
+ EVIDENCE_SEARCH_DEFAULT_CANDIDATES,
11
+ EVIDENCE_SEARCH_DEFAULT_POSTING_BUDGET,
12
+ EVIDENCE_SEARCH_MAX_CANDIDATES,
13
+ EVIDENCE_SEARCH_MAX_POSTING_BUDGET,
14
+ searchEvidenceCandidates,
15
+ } from '../../vault/src/evidence-search-index.mjs';
16
+
17
+ export const MCP_EVIDENCE_RECALL_MAX_BYTES = 512 * 1024;
18
+
19
+ function integer(value, fallback, { min = 1, max } = {}) {
20
+ const number = Number(value ?? fallback);
21
+ if (!Number.isSafeInteger(number) || number < min || number > max) {
22
+ throw new RangeError(`value must be an integer between ${min} and ${max}`);
23
+ }
24
+ return number;
25
+ }
26
+
27
+ function backend(value) {
28
+ const normalized = String(value ?? 'auto').trim().toLowerCase();
29
+ if (!['auto', 'sqlite', 'lexical'].includes(normalized)) {
30
+ throw new TypeError('backend must be auto, sqlite, or lexical');
31
+ }
32
+ return normalized;
33
+ }
34
+
35
+ function filters(value) {
36
+ if (value === undefined || value === null) return {};
37
+ if (typeof value !== 'object' || Array.isArray(value)) {
38
+ throw new TypeError('filters must be an object');
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function logicalReference(result) {
44
+ const { logical_path: logicalPath, ...rest } = result || {};
45
+ return {
46
+ ...rest,
47
+ logical_ref: String(logicalPath || ''),
48
+ };
49
+ }
50
+
51
+ function mappedError(error) {
52
+ if (String(error?.code || '').startsWith('MCP_')) return error;
53
+ let code = 'MCP_EVIDENCE_RECALL_FAILED';
54
+ if (error instanceof EvidenceRecallCursorError
55
+ || error?.code === 'EVIDENCE_RECALL_CURSOR_INVALID') {
56
+ code = 'MCP_EVIDENCE_CURSOR_INVALID';
57
+ } else if (error instanceof EvidenceRecallBudgetError
58
+ || error?.code === 'EVIDENCE_RECALL_BUDGET_TOO_SMALL') {
59
+ code = 'MCP_EVIDENCE_BUDGET_TOO_SMALL';
60
+ } else if (error?.code === 'EVIDENCE_SEARCH_SQLITE_UNAVAILABLE'
61
+ || error?.code === 'EVIDENCE_SEARCH_FTS5_UNAVAILABLE') {
62
+ code = 'MCP_EVIDENCE_BACKEND_UNAVAILABLE';
63
+ } else if (error?.code === 'VAULT_PATH_UNSAFE') {
64
+ code = 'MCP_EVIDENCE_ARTIFACT_UNSAFE';
65
+ } else if (error instanceof TypeError || error instanceof RangeError) {
66
+ code = 'MCP_EVIDENCE_RECALL_INVALID';
67
+ }
68
+ return Object.assign(new Error(error?.message || 'evidence recall failed'), {
69
+ code,
70
+ cause: error,
71
+ });
72
+ }
73
+
74
+ export function recallEvidenceForMcp(vaultBase, args = {}) {
75
+ try {
76
+ const query = String(args.query || '').trim();
77
+ if (!query) {
78
+ throw Object.assign(new Error('query is required'), {
79
+ code: 'MCP_EVIDENCE_QUERY_REQUIRED',
80
+ });
81
+ }
82
+ const limit = integer(args.limit, EVIDENCE_RECALL_DEFAULT_LIMIT, {
83
+ max: EVIDENCE_RECALL_MAX_LIMIT,
84
+ });
85
+ const maxBytes = integer(args.max_bytes, EVIDENCE_RECALL_DEFAULT_MAX_BYTES, {
86
+ min: 2,
87
+ max: MCP_EVIDENCE_RECALL_MAX_BYTES,
88
+ });
89
+ const candidateLimit = integer(
90
+ args.candidate_limit,
91
+ Math.max(EVIDENCE_SEARCH_DEFAULT_CANDIDATES, limit),
92
+ { max: EVIDENCE_SEARCH_MAX_CANDIDATES },
93
+ );
94
+ const postingBudget = integer(
95
+ args.posting_budget,
96
+ EVIDENCE_SEARCH_DEFAULT_POSTING_BUDGET,
97
+ { max: EVIDENCE_SEARCH_MAX_POSTING_BUDGET },
98
+ );
99
+ const normalizedFilters = filters(args.filters);
100
+ const candidates = searchEvidenceCandidates(vaultBase, query, {
101
+ candidateLimit,
102
+ postingBudget,
103
+ filters: normalizedFilters,
104
+ backend: backend(args.backend),
105
+ sqlite: 'auto',
106
+ });
107
+ const page = recallEvidencePage(candidates.rows, query, {
108
+ cursor: args.cursor || null,
109
+ filters: normalizedFilters,
110
+ limit,
111
+ maxBytes,
112
+ });
113
+ return {
114
+ schema_version: 1,
115
+ ...page,
116
+ results: page.results.map(logicalReference),
117
+ candidates: {
118
+ backend: candidates.backend,
119
+ count: candidates.candidate_count,
120
+ posting_entries: candidates.posting_entries,
121
+ has_more: candidates.has_more,
122
+ rebuilt: candidates.rebuilt,
123
+ fallback_reason: candidates.fallback_reason || '',
124
+ },
125
+ complete_candidate_set: candidates.has_more !== true,
126
+ };
127
+ } catch (error) {
128
+ throw mappedError(error);
129
+ }
130
+ }
@@ -23,6 +23,7 @@ import {
23
23
  } from '../../vault/src/memory-store.mjs';
24
24
  import { scopeForMemoryKey } from '../../vault/src/memory-scope.mjs';
25
25
  import { sanitizeMemoryText } from '../../vault/src/memory-schema.mjs';
26
+ import { recallEvidenceForMcp } from './evidence-recall.mjs';
26
27
 
27
28
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
28
29
  const BIN = join(ROOT, 'bin', 'wendkeep.mjs');
@@ -305,6 +306,9 @@ export async function executeNativeMcpTool(tool, args, { signal } = {}) {
305
306
  topK: Math.min(Number(args.limit || 10), 100),
306
307
  }));
307
308
  }
309
+ if (tool.name === 'wendkeep_evidence_recall') {
310
+ return sanitize(recallEvidenceForMcp(ctx.vaultBase, args));
311
+ }
308
312
  if (tool.name === 'wendkeep_memory_conflicts') {
309
313
  return sanitize(listMemoryCandidates(ctx.vaultBase, { activeOnly: true }).candidates);
310
314
  }
@@ -3,6 +3,9 @@ import { MCP_EFFECT_MANIFEST, resolveMcpToolEffect } from './effects.mjs';
3
3
  const DEFAULT_PAGE_SIZE = 50;
4
4
  const MAX_PAGE_SIZE = 100;
5
5
  const DEFAULT_TIMEOUT_MS = 10_000;
6
+ const MAX_EVIDENCE_BYTES = 512 * 1024;
7
+ const MAX_EVIDENCE_CANDIDATES = 4096;
8
+ const MAX_EVIDENCE_POSTINGS = 1_048_576;
6
9
 
7
10
  function boundedInteger(value, fallback, maximum = MAX_PAGE_SIZE) {
8
11
  const parsed = Number.parseInt(value, 10);
@@ -42,6 +45,7 @@ function availability(tool, nodeVersion) {
42
45
 
43
46
  const TOOL_REQUIRED_ARGUMENTS = Object.freeze({
44
47
  wendkeep_context_status: ['session_id'],
48
+ wendkeep_evidence_recall: ['query'],
45
49
  wendkeep_change_show: ['change'],
46
50
  wendkeep_change_status: ['change'],
47
51
  wendkeep_task_show: ['session_id', 'task'],
@@ -55,6 +59,13 @@ const TOOL_REQUIRED_ARGUMENTS = Object.freeze({
55
59
  wendkeep_handoff_publish: ['payload'],
56
60
  });
57
61
 
62
+ const stringOrStringList = {
63
+ oneOf: [
64
+ { type: 'string' },
65
+ { type: 'array', items: { type: 'string' }, uniqueItems: true },
66
+ ],
67
+ };
68
+
58
69
  function inputSchema(tool) {
59
70
  const required = ['project_root'];
60
71
  required.push(...(TOOL_REQUIRED_ARGUMENTS[tool.name] || []));
@@ -87,6 +98,25 @@ function inputSchema(tool) {
87
98
  query: { type: 'string' },
88
99
  cursor: { type: 'string' },
89
100
  limit: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
101
+ max_bytes: { type: 'integer', minimum: 2, maximum: MAX_EVIDENCE_BYTES },
102
+ candidate_limit: { type: 'integer', minimum: 1, maximum: MAX_EVIDENCE_CANDIDATES },
103
+ posting_budget: { type: 'integer', minimum: 1, maximum: MAX_EVIDENCE_POSTINGS },
104
+ backend: { type: 'string', enum: ['auto', 'sqlite', 'lexical'] },
105
+ filters: {
106
+ type: 'object',
107
+ additionalProperties: false,
108
+ properties: {
109
+ authority: stringOrStringList,
110
+ validity: stringOrStringList,
111
+ entity_type: stringOrStringList,
112
+ project_id: stringOrStringList,
113
+ change_slug: stringOrStringList,
114
+ session_id: stringOrStringList,
115
+ work_session_id: stringOrStringList,
116
+ logical_path: stringOrStringList,
117
+ logical_path_prefix: stringOrStringList,
118
+ },
119
+ },
90
120
  payload: { type: 'object' },
91
121
  },
92
122
  };
@@ -339,4 +369,4 @@ export function createNativeMcpServer({
339
369
  };
340
370
  }
341
371
 
342
- export { supportsObserverSql };
372
+ export { supportsObserverSql };