wendkeep 0.80.2 → 0.85.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 (75) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.en.md +28 -11
  3. package/README.md +28 -11
  4. package/docs/en/commands/capabilities.md +82 -0
  5. package/docs/en/commands/getting-started.md +3 -1
  6. package/docs/en/commands/mcp.md +99 -0
  7. package/docs/en/commands/portable.md +88 -0
  8. package/docs/en/commands/sync-protocol.md +58 -0
  9. package/docs/en/commands/tdd.md +96 -0
  10. package/docs/en/commands/verify.md +5 -0
  11. package/docs/pt-BR/commands/capabilities.md +82 -0
  12. package/docs/pt-BR/commands/getting-started.md +3 -2
  13. package/docs/pt-BR/commands/mcp.md +99 -0
  14. package/docs/pt-BR/commands/portable.md +87 -0
  15. package/docs/pt-BR/commands/sync-protocol.md +58 -0
  16. package/docs/pt-BR/commands/tdd.md +96 -0
  17. package/docs/pt-BR/commands/verify.md +5 -0
  18. package/hooks/active-context-store.mjs +2 -0
  19. package/hooks/change-core.mjs +5 -0
  20. package/hooks/project-scope.mjs +2 -1
  21. package/hooks/session-ensure.mjs +23 -7
  22. package/hooks/session-start.mjs +20 -5
  23. package/package.json +3 -3
  24. package/packages/cli/src/index.mjs +42 -2
  25. package/packages/harness/src/sensors-core.mjs +16 -3
  26. package/packages/integrations/src/capabilities.mjs +220 -0
  27. package/packages/integrations/src/index.mjs +1 -0
  28. package/packages/mcp/src/audit.mjs +49 -0
  29. package/packages/mcp/src/cli.mjs +78 -0
  30. package/packages/mcp/src/config.mjs +22 -1
  31. package/packages/mcp/src/effects.mjs +115 -0
  32. package/packages/mcp/src/executor.mjs +354 -0
  33. package/packages/mcp/src/index.mjs +7 -0
  34. package/packages/mcp/src/server.mjs +342 -0
  35. package/packages/mcp/src/stdio.mjs +38 -0
  36. package/packages/mcp/src/sync.mjs +56 -0
  37. package/packages/pi/package.json +2 -1
  38. package/packages/pi/src/index.mjs +29 -0
  39. package/schema/handoff-contract-v1.schema.json +4 -0
  40. package/schema/host-capability-manifest-v1.schema.json +46 -0
  41. package/schema/host-coverage-v1.schema.json +55 -0
  42. package/schema/mcp-effect-manifest-v1.schema.json +36 -0
  43. package/schema/mcp-tool-input-v1.schema.json +32 -0
  44. package/schema/mcp-tool-result-v1.schema.json +22 -0
  45. package/schema/portable-active-work-v1.schema.json +38 -0
  46. package/schema/portable-state-v1.schema.json +36 -0
  47. package/schema/sync-event-v1.schema.json +25 -0
  48. package/schema/sync-private-envelope-v1.schema.json +16 -0
  49. package/schema/sync-state-v1.schema.json +18 -0
  50. package/schema/task-contract-v1.schema.json +2 -0
  51. package/schema/tdd-attestation-v1.schema.json +39 -0
  52. package/schema/wendkeep.evidence-envelope-v2.schema.json +17 -0
  53. package/schema/wendkeep.sensors.schema.json +19 -0
  54. package/src/active-context-runtime.mjs +1 -0
  55. package/src/capabilities.mjs +50 -0
  56. package/src/doctor.mjs +28 -0
  57. package/src/evidence-envelope.mjs +12 -6
  58. package/src/host-capabilities.mjs +34 -0
  59. package/src/init.mjs +3 -3
  60. package/src/mcp.mjs +7 -0
  61. package/src/observer-snapshot.mjs +25 -0
  62. package/src/portable.mjs +558 -0
  63. package/src/skills-seed.mjs +26 -0
  64. package/src/sync-adapters.mjs +188 -0
  65. package/src/sync-outbox.mjs +155 -0
  66. package/src/sync-protocol-cli.mjs +277 -0
  67. package/src/sync-protocol.mjs +368 -0
  68. package/src/sync.mjs +8 -0
  69. package/src/task-contracts.mjs +67 -2
  70. package/src/task.mjs +5 -1
  71. package/src/tdd-attestation-store.mjs +98 -0
  72. package/src/tdd-attestation.mjs +254 -0
  73. package/src/tdd.mjs +198 -0
  74. package/src/vault-readme.mjs +4 -4
  75. package/src/verify.mjs +24 -0
@@ -0,0 +1,254 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { sanitizeMemoryText } from '../packages/vault/src/memory-schema.mjs';
4
+
5
+ const OUTPUT_TAIL_LIMIT = 2_000;
6
+ const COMMAND_LIMIT = 1_000;
7
+ const IDENTITY_FIELDS = [
8
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
9
+ ];
10
+ const INFRASTRUCTURE_FAILURES = [
11
+ /cannot find module/i,
12
+ /module not found/i,
13
+ /cannot find package/i,
14
+ /syntaxerror/i,
15
+ /enoent/i,
16
+ /command not found/i,
17
+ /is not recognized as an internal or external command/i,
18
+ /failed to load (?:config|configuration)/i,
19
+ /configuration (?:error|invalid|missing)/i,
20
+ /unknown (?:option|argument)/i,
21
+ ];
22
+
23
+ function stableValue(value) {
24
+ if (Array.isArray(value)) return value.map(stableValue);
25
+ if (!value || typeof value !== 'object') return value;
26
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
27
+ }
28
+
29
+ function sha256(value) {
30
+ return `sha256:${createHash('sha256').update(String(value || ''), 'utf8').digest('hex')}`;
31
+ }
32
+
33
+ function canonicalId(value) {
34
+ return createHash('sha256').update(JSON.stringify(stableValue(value)), 'utf8').digest('hex');
35
+ }
36
+
37
+ function uniqueStrings(values) {
38
+ const input = Array.isArray(values) ? values : [values];
39
+ return [...new Set(input.map((value) => String(value || '').trim()).filter(Boolean))];
40
+ }
41
+
42
+ function normalizedPath(value) {
43
+ const path = String(value || '').replaceAll('\\', '/').replace(/^\.\//, '').trim();
44
+ if (!path || path.startsWith('/') || /^[A-Za-z]:\//.test(path) || path.split('/').includes('..')) {
45
+ throw Object.assign(new Error(`invalid test path: ${value}`), { code: 'TDD_TEST_PATH_INVALID' });
46
+ }
47
+ return path;
48
+ }
49
+
50
+ function testPathsOf(values) {
51
+ return uniqueStrings(values).map(normalizedPath).sort();
52
+ }
53
+
54
+ function sanitizeDiagnostic(value) {
55
+ return sanitizeMemoryText(String(value || ''))
56
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
57
+ .replace(/file:\/\/\/[A-Za-z]:\/[^\s)]+/gi, '[LOCAL_FILE]')
58
+ .replace(/"[A-Za-z]:\\[^"\r\n]+"/g, '"[LOCAL_EXECUTABLE]"')
59
+ .replace(/'[A-Za-z]:\\+[^'\r\n]+'/g, "'[LOCAL_FILE]'")
60
+ .replace(/\b[A-Za-z]:\\+[^\s)"'\r\n]+/g, '[LOCAL_FILE]')
61
+ .replace(/\/[Uu]sers\/[^/\s]+\/[^\s)]+/g, '[LOCAL_FILE]')
62
+ .replace(/\bgh[pousr]_[A-Za-z0-9_]{12,}\b/g, '[REDACTED_SECRET]')
63
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{12,}\b/g, '[REDACTED_SECRET]')
64
+ .replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@')
65
+ .replace(/\r/g, '')
66
+ .trim();
67
+ }
68
+
69
+ function boundedTail(value) {
70
+ const sanitized = sanitizeDiagnostic(value);
71
+ return sanitized.length <= OUTPUT_TAIL_LIMIT
72
+ ? sanitized
73
+ : `…${sanitized.slice(-(OUTPUT_TAIL_LIMIT - 1))}`;
74
+ }
75
+
76
+ function commandObservation(command, result, observedAt) {
77
+ const rawOutput = [result?.stdout, result?.stderr, result?.error?.message].filter(Boolean).join('\n');
78
+ return {
79
+ command: sanitizeDiagnostic(command).slice(0, COMMAND_LIMIT),
80
+ command_digest: sha256(command),
81
+ exit_code: Number.isInteger(result?.status) ? result.status : null,
82
+ output_digest: sha256(rawOutput),
83
+ output_tail: boundedTail(rawOutput),
84
+ observed_at: String(observedAt || new Date().toISOString()),
85
+ };
86
+ }
87
+
88
+ function snapshotObservation(snapshot = {}) {
89
+ return {
90
+ branch: String(snapshot.branch || ''),
91
+ head_sha: String(snapshot.head_sha || ''),
92
+ index_tree_sha: String(snapshot.index_tree_sha || ''),
93
+ worktree_digest: String(snapshot.worktree_digest || ''),
94
+ change_manifest: stableValue(snapshot.change_manifest || {}),
95
+ };
96
+ }
97
+
98
+ function attestationIdentity({ identity = {}, taskId, requirementId }) {
99
+ const normalizedIdentity = Object.fromEntries(
100
+ IDENTITY_FIELDS.map((field) => [field, String(identity[field] || '').trim()]),
101
+ );
102
+ if (Object.values(normalizedIdentity).some((value) => !value)
103
+ || !String(taskId || '').trim() || !String(requirementId || '').trim()) {
104
+ throw Object.assign(new Error('causal identity, task and requirement are required'), {
105
+ code: 'TDD_IDENTITY_REQUIRED',
106
+ });
107
+ }
108
+ return {
109
+ ...normalizedIdentity,
110
+ task_id: String(taskId).trim(),
111
+ requirement_id: String(requirementId).trim(),
112
+ };
113
+ }
114
+
115
+ function baseAttestation(input) {
116
+ const causal = attestationIdentity(input);
117
+ return {
118
+ schema_version: 1,
119
+ attestation_id: canonicalId(causal),
120
+ ...causal,
121
+ profile: String(input.profile || 'OFF').toUpperCase(),
122
+ state: 'invalid',
123
+ test_paths: testPathsOf(input.testPaths),
124
+ red: null,
125
+ green: null,
126
+ green_history: [],
127
+ waiver: null,
128
+ review_flags: [],
129
+ invalid_reason: null,
130
+ };
131
+ }
132
+
133
+ function invalid(attestation, reason) {
134
+ return { ...attestation, state: 'invalid', invalid_reason: reason };
135
+ }
136
+
137
+ export function classifyTddRedResult(result = {}) {
138
+ if ((result.status ?? 1) === 0) return { valid: false, reason: 'TDD_RED_ALREADY_GREEN' };
139
+ const diagnostic = [result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n');
140
+ if (INFRASTRUCTURE_FAILURES.some((pattern) => pattern.test(diagnostic))) {
141
+ return { valid: false, reason: 'TDD_RED_INFRASTRUCTURE_FAILURE' };
142
+ }
143
+ return { valid: true, reason: null };
144
+ }
145
+
146
+ export function createRedAttestation(input = {}) {
147
+ const base = baseAttestation(input);
148
+ const execution = commandObservation(input.command, input.result, input.observedAt);
149
+ const red = {
150
+ ...snapshotObservation(input.snapshot),
151
+ ...execution,
152
+ failure_digest: execution.output_digest,
153
+ };
154
+ delete red.output_digest;
155
+ const observed = { ...base, red };
156
+ const classification = classifyTddRedResult(input.result);
157
+ return classification.valid
158
+ ? { ...observed, state: 'red-observed', invalid_reason: null }
159
+ : invalid(observed, classification.reason);
160
+ }
161
+
162
+ function sameIdentity(attestation, input) {
163
+ return IDENTITY_FIELDS.every((field) => (
164
+ String(attestation?.[field] || '') === String(input.identity?.[field] || '')
165
+ ))
166
+ && String(attestation?.task_id || '') === String(input.taskId || '')
167
+ && String(attestation?.requirement_id || '') === String(input.requirementId || '');
168
+ }
169
+
170
+ function changedManifestPaths(left = {}, right = {}) {
171
+ const paths = new Set([...Object.keys(left || {}), ...Object.keys(right || {})]);
172
+ return [...paths].filter((path) => String(left?.[path] || '') !== String(right?.[path] || '')).sort();
173
+ }
174
+
175
+ function isDeclaredTestPath(path, declared) {
176
+ const normalized = normalizedPath(path);
177
+ return declared.some((testPath) => normalized === testPath || normalized.startsWith(`${testPath}/`));
178
+ }
179
+
180
+ export function completeGreenAttestation(attestation, input = {}) {
181
+ const current = { ...attestation, green_history: [...(attestation?.green_history || [])] };
182
+ if (!sameIdentity(current, input)
183
+ || String(current.red?.branch || '') !== String(input.snapshot?.branch || '')) {
184
+ return invalid(current, 'TDD_CAUSAL_IDENTITY_MISMATCH');
185
+ }
186
+ if (!current.red || !['red-observed', 'green-observed'].includes(String(current.state || ''))) {
187
+ return invalid(current, 'TDD_RED_REQUIRED');
188
+ }
189
+ if ((input.result?.status ?? 1) !== 0) return invalid(current, 'TDD_GREEN_FAILED');
190
+ if (input.isAncestor !== true) return invalid(current, 'TDD_GREEN_NOT_CAUSAL_SUCCESSOR');
191
+
192
+ const nextTestPaths = testPathsOf(input.testPaths?.length ? input.testPaths : current.test_paths);
193
+ const allTestPaths = uniqueStrings([...current.test_paths, ...nextTestPaths]).map(normalizedPath);
194
+ const changedPaths = uniqueStrings([
195
+ ...changedManifestPaths(current.red.change_manifest, input.snapshot?.change_manifest),
196
+ ...(input.committedPaths || []),
197
+ ]).map(normalizedPath).sort();
198
+ const productionPaths = changedPaths.filter((path) => !isDeclaredTestPath(path, allTestPaths));
199
+ if (!productionPaths.length) return invalid(current, 'TDD_IMPLEMENTATION_NOT_AFTER_RED');
200
+
201
+ const execution = commandObservation(input.command, input.result, input.observedAt);
202
+ const green = {
203
+ ...snapshotObservation(input.snapshot),
204
+ ...execution,
205
+ result_digest: execution.output_digest,
206
+ production_paths: productionPaths,
207
+ };
208
+ delete green.output_digest;
209
+ if (current.green) current.green_history.push(current.green);
210
+ const testPathsChanged = JSON.stringify(current.test_paths) !== JSON.stringify(nextTestPaths);
211
+ return {
212
+ ...current,
213
+ state: 'green-observed',
214
+ invalid_reason: null,
215
+ test_paths: nextTestPaths,
216
+ green,
217
+ green_history: current.green_history,
218
+ review_flags: testPathsChanged ? ['TDD_TEST_PATHS_CHANGED'] : [],
219
+ };
220
+ }
221
+
222
+ export function evaluateTddAttestation(attestation, snapshot = {}, options = {}) {
223
+ if (!attestation || !['green-observed', 'waived'].includes(attestation.state)) {
224
+ return attestation || { state: 'invalid', invalid_reason: 'TDD_ATTESTATION_MISSING' };
225
+ }
226
+ if (attestation.state === 'waived') return attestation;
227
+ if (Array.isArray(options.mutationSurvivors) && options.mutationSurvivors.length) {
228
+ return invalid(attestation, 'TDD_MUTATION_SURVIVOR');
229
+ }
230
+ const green = attestation.green || {};
231
+ const stale = ['head_sha', 'index_tree_sha', 'worktree_digest']
232
+ .some((field) => String(green[field] || '') !== String(snapshot[field] || ''));
233
+ return stale ? invalid(attestation, 'TDD_GREEN_STALE_AFTER_REFACTOR') : attestation;
234
+ }
235
+
236
+ export function waiveTddAttestation(input = {}) {
237
+ const authority = sanitizeDiagnostic(input.authority);
238
+ const reason = sanitizeDiagnostic(input.reason);
239
+ if (!authority || !reason) {
240
+ throw Object.assign(new Error('waiver requires explicit human authority and reason'), {
241
+ code: 'TDD_WAIVER_AUTHORITY_REQUIRED',
242
+ });
243
+ }
244
+ return {
245
+ ...baseAttestation(input),
246
+ state: 'waived',
247
+ invalid_reason: null,
248
+ waiver: {
249
+ authority: authority.slice(0, 200),
250
+ reason: reason.slice(0, 1_000),
251
+ observed_at: String(input.observedAt || new Date().toISOString()),
252
+ },
253
+ };
254
+ }
package/src/tdd.mjs ADDED
@@ -0,0 +1,198 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ import { activeChange } from '../hooks/change-core.mjs';
5
+ import { resolveActiveContext } from '../hooks/active-context-store.mjs';
6
+ import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
7
+ import { findProjectRoot } from '../packages/harness/src/sensors-core.mjs';
8
+ import { resolveCommandActiveContext } from './active-context-runtime.mjs';
9
+ import { buildTaskContractSnapshot } from './task-contracts.mjs';
10
+ import {
11
+ completeGreenAttestation,
12
+ createRedAttestation,
13
+ evaluateTddAttestation,
14
+ waiveTddAttestation,
15
+ } from './tdd-attestation.mjs';
16
+ import {
17
+ captureTddSnapshot,
18
+ committedPathsBetween,
19
+ isGitAncestor,
20
+ readTddAttestationStore,
21
+ saveTddAttestation,
22
+ } from './tdd-attestation-store.mjs';
23
+
24
+ const HELP = `wendkeep tdd <red|green|status|waive> <task-id>
25
+
26
+ --requirement <id> requirement bound to the task
27
+ --test <path> test path (repeatable)
28
+ --command <command> discriminating test command
29
+ --reason <text> waiver reason
30
+ --authority <id> explicit human waiver authority
31
+ --session <id> causal work session
32
+ --change <slug> active change override
33
+ --json emit structured JSON
34
+ `;
35
+
36
+ function opt(argv, name) {
37
+ const index = argv.indexOf(name);
38
+ return index >= 0 ? argv[index + 1] || '' : '';
39
+ }
40
+
41
+ function opts(argv, name) {
42
+ const values = [];
43
+ for (let index = 0; index < argv.length; index += 1) {
44
+ if (argv[index] === name && argv[index + 1]) values.push(argv[index + 1]);
45
+ }
46
+ return values;
47
+ }
48
+
49
+ function tddError(code, message) {
50
+ return Object.assign(new Error(message), { code });
51
+ }
52
+
53
+ function commandState(argv) {
54
+ const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
55
+ if (!vaultRaw) throw tddError('TDD_VAULT_MISSING', 'no vault (--vault or OBSIDIAN_VAULT_PATH)');
56
+ const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
57
+ const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
58
+ const requestedSession = opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '';
59
+ const identity = resolveCommandActiveContext({
60
+ vaultBase, projectRoot, sessionId: requestedSession, requireExisting: true,
61
+ });
62
+ if (!identity) throw tddError('TDD_ACTIVE_CONTEXT_NOT_FOUND', 'active context is required');
63
+ const context = resolveActiveContext(vaultBase, identity);
64
+ const explicitChange = opt(argv, '--change');
65
+ const changeSlug = explicitChange || activeChange(vaultBase, { context: identity });
66
+ if (!changeSlug) throw tddError('TDD_CHANGE_NOT_FOUND', 'no active change');
67
+ if (explicitChange && context.change_slug && explicitChange !== context.change_slug) {
68
+ throw tddError('TDD_CHANGE_CONTEXT_MISMATCH', 'requested change differs from active context');
69
+ }
70
+ const runtime = resolveHookOperatingProfile({
71
+ input: { cwd: projectRoot, session_id: identity.sessionId || requestedSession },
72
+ });
73
+ return {
74
+ vaultBase, projectRoot, identity, context, changeSlug,
75
+ profile: runtime.profile,
76
+ json: argv.includes('--json'),
77
+ };
78
+ }
79
+
80
+ function causalIdentity(state) {
81
+ return {
82
+ project_id: state.identity.projectId,
83
+ repository_id: state.identity.repositoryId,
84
+ worktree_id: state.identity.worktreeId,
85
+ work_session_id: state.identity.workSessionId,
86
+ change_slug: state.changeSlug,
87
+ };
88
+ }
89
+
90
+ function taskAndRequirement(state, taskId, requested = '') {
91
+ const snapshot = buildTaskContractSnapshot({ ...state, profile: state.profile });
92
+ const task = snapshot.contracts.find((item) => item.task_id === taskId);
93
+ if (!task) throw tddError('TDD_TASK_NOT_FOUND', `task not found: ${taskId || '(missing id)'}`);
94
+ const requirementId = String(requested || task.requirement_ids[0] || '').trim();
95
+ if (!requirementId || !task.requirement_ids.includes(requirementId)) {
96
+ throw tddError('TDD_REQUIREMENT_NOT_BOUND', `requirement is not bound to task ${taskId}`);
97
+ }
98
+ return { task, requirementId };
99
+ }
100
+
101
+ function runObserved(command, projectRoot) {
102
+ if (!String(command || '').trim()) throw tddError('TDD_COMMAND_REQUIRED', '--command is required');
103
+ const env = { ...process.env };
104
+ // A test command launched by the CLI is a new observation, not a recursive
105
+ // child of WendKeep's own node:test process.
106
+ delete env.NODE_TEST_CONTEXT;
107
+ return spawnSync(command, [], {
108
+ cwd: projectRoot,
109
+ shell: true,
110
+ encoding: 'utf8',
111
+ windowsHide: true,
112
+ maxBuffer: 8 * 1024 * 1024,
113
+ env,
114
+ });
115
+ }
116
+
117
+ function currentAttestation(state, taskId) {
118
+ return readTddAttestationStore(state.vaultBase, state.changeSlug).attestations
119
+ .find((item) => item.task_id === taskId) || null;
120
+ }
121
+
122
+ function write(value, json) {
123
+ process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${value.task_id}: ${value.state}\n`);
124
+ }
125
+
126
+ function fail(error, json) {
127
+ const payload = { ok: false, code: error?.code || 'TDD_COMMAND_FAILED', error: String(error?.message || error) };
128
+ process.stderr.write(json ? `${JSON.stringify(payload)}\n` : `wendkeep tdd: ${payload.code}: ${payload.error}\n`);
129
+ return 2;
130
+ }
131
+
132
+ export function runTdd(argv = []) {
133
+ const sub = argv[0];
134
+ if (!sub || ['help', '--help', '-h'].includes(sub)) {
135
+ process.stdout.write(HELP);
136
+ return 0;
137
+ }
138
+ const json = argv.includes('--json');
139
+ try {
140
+ const state = commandState(argv);
141
+ const taskId = String(argv[1] || '').trim();
142
+ if (!taskId) throw tddError('TDD_TASK_REQUIRED', 'task id is required');
143
+
144
+ if (sub === 'red') {
145
+ const { requirementId } = taskAndRequirement(state, taskId, opt(argv, '--requirement'));
146
+ const testPaths = opts(argv, '--test');
147
+ if (!testPaths.length) throw tddError('TDD_TEST_PATH_REQUIRED', 'at least one --test is required');
148
+ const command = opt(argv, '--command');
149
+ const attestation = createRedAttestation({
150
+ identity: causalIdentity(state), taskId, requirementId, testPaths,
151
+ profile: state.profile, command, result: runObserved(command, state.projectRoot),
152
+ snapshot: captureTddSnapshot(state.projectRoot),
153
+ });
154
+ saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
155
+ write(attestation, state.json);
156
+ return attestation.state === 'red-observed' ? 0 : 1;
157
+ }
158
+
159
+ if (sub === 'green') {
160
+ const current = currentAttestation(state, taskId);
161
+ if (!current) throw tddError('TDD_RED_REQUIRED', 'no RED attestation exists for this task');
162
+ const command = opt(argv, '--command');
163
+ const snapshot = captureTddSnapshot(state.projectRoot);
164
+ const attestation = completeGreenAttestation(current, {
165
+ identity: causalIdentity(state), taskId, requirementId: current.requirement_id,
166
+ testPaths: opts(argv, '--test'), command,
167
+ result: runObserved(command, state.projectRoot), snapshot,
168
+ isAncestor: isGitAncestor(state.projectRoot, current.red?.head_sha, snapshot.head_sha),
169
+ committedPaths: committedPathsBetween(state.projectRoot, current.red?.head_sha, snapshot.head_sha),
170
+ });
171
+ saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
172
+ write(attestation, state.json);
173
+ return attestation.state === 'green-observed' ? 0 : 1;
174
+ }
175
+
176
+ if (sub === 'waive') {
177
+ const { requirementId } = taskAndRequirement(state, taskId, opt(argv, '--requirement'));
178
+ const attestation = waiveTddAttestation({
179
+ identity: causalIdentity(state), taskId, requirementId, profile: state.profile,
180
+ reason: opt(argv, '--reason'), authority: opt(argv, '--authority'),
181
+ });
182
+ saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
183
+ write(attestation, state.json);
184
+ return 0;
185
+ }
186
+
187
+ if (sub === 'status') {
188
+ const current = currentAttestation(state, taskId);
189
+ if (!current) throw tddError('TDD_ATTESTATION_NOT_FOUND', `no attestation for task ${taskId}`);
190
+ const evaluated = evaluateTddAttestation(current, captureTddSnapshot(state.projectRoot));
191
+ write(evaluated, state.json);
192
+ return ['green-observed', 'waived'].includes(evaluated.state) ? 0 : 1;
193
+ }
194
+ throw tddError('TDD_SUBCOMMAND_UNKNOWN', `unknown subcommand: ${sub}`);
195
+ } catch (error) {
196
+ return fail(error, json);
197
+ }
198
+ }
@@ -49,9 +49,9 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, loca
49
49
  const table = [en ? '| Folder | Contents |' : '| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
50
50
 
51
51
  if (en) {
52
- const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
52
+ const mcpIntro = withMcp ? ', and queried through the native semantic **WendKeep MCP** server' : '';
53
53
  const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
54
- if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
54
+ if (withMcp) access.push('- **Agent (MCP):** the native `wendkeep-vault` server points at this vault (set in `.mcp.json`), exposing bounded semantic reads and capability-gated writes without arbitrary filesystem access.');
55
55
  access.push('- **Hooks:** Codex and Claude Code call `npx --no-install wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
56
56
  return `# Obsidian vault — ${name}
57
57
 
@@ -81,9 +81,9 @@ ${access.join('\n')}
81
81
  `;
82
82
  }
83
83
 
84
- const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
84
+ const mcpIntro = withMcp ? ', e consultada pelo servidor semântico nativo **WendKeep MCP**' : '';
85
85
  const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
86
- if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
86
+ if (withMcp) access.push('- **Agente (MCP):** o servidor nativo `wendkeep-vault` aponta para este vault pelo `wendkeep init` (em `.mcp.json`), expondo leituras semânticas bounded e writes por capability, sem acesso arbitrário ao filesystem.');
87
87
  access.push('- **Hooks:** Codex e Claude Code chamam `npx --no-install wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
88
88
  return `# Vault Obsidian — ${name}
89
89
 
package/src/verify.mjs CHANGED
@@ -22,7 +22,11 @@ import {
22
22
  import { addLesson } from '../hooks/lessons-core.mjs';
23
23
  import { getLocale } from '../hooks/locale.mjs';
24
24
  import { resolveCommandActiveContext } from './active-context-runtime.mjs';
25
+ import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
26
+ import { evaluateTddAttestation } from './tdd-attestation.mjs';
27
+ import { readTddAttestationStore } from './tdd-attestation-store.mjs';
25
28
  import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
29
+ import { evaluateHostCoverage } from '../packages/integrations/src/capabilities.mjs';
26
30
  import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
27
31
  import {
28
32
  assertStableHead,
@@ -103,6 +107,16 @@ export function runVerify(argv) {
103
107
  process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
104
108
  }
105
109
  const sensors = loaded.sensors;
110
+ const hostGate = evaluateHostCoverage(
111
+ commandContext?.hostCoverage || null,
112
+ loaded.requiredHostCapabilities,
113
+ { waivers: loaded.hostCapabilityWaivers },
114
+ );
115
+ if (!hostGate.ok) {
116
+ const missing = hostGate.findings.map((item) => `${item.capability}:${item.state}`).join(', ');
117
+ process.stderr.write(`wendkeep verify: HOST_CAPABILITY_UNAVAILABLE: ${missing}\n`);
118
+ process.exit(1);
119
+ }
106
120
  const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
107
121
  const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
108
122
  const tasksHash = tasksHashOf(tarefas);
@@ -126,6 +140,9 @@ export function runVerify(argv) {
126
140
  cwd: projectRoot,
127
141
  env: sensorProcessEnv(vaultBase),
128
142
  });
143
+ const mutationSurvivors = evidence.flatMap((sensor) => sensor.survivors ?? []);
144
+ const tddAttestations = readTddAttestationStore(vaultBase, slug).attestations
145
+ .map((attestation) => evaluateTddAttestation(attestation, startSnapshot, { mutationSurvivors }));
129
146
  let finishSnapshot;
130
147
  try {
131
148
  finishSnapshot = captureGitSnapshot(projectRoot);
@@ -142,8 +159,10 @@ export function runVerify(argv) {
142
159
  effectiveSpecSha256: `sha256:${effective.hash}`,
143
160
  sensorConfigSha256: sensorConfigSha256(sensors, ids),
144
161
  sensors: evidence,
162
+ tddAttestations,
145
163
  startedAt,
146
164
  finishedAt: new Date().toISOString(),
165
+ hostCoverage: commandContext?.hostCoverage || null,
147
166
  });
148
167
  writeAuthority('evidencia.json', `${JSON.stringify(envelope, null, 2)}\n`);
149
168
  // Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
@@ -195,11 +214,15 @@ export function runVerify(argv) {
195
214
  // verify can announce success or assemble the deep package. Legacy projects without an active
196
215
  // context preserve their pre-contract behavior until they migrate.
197
216
  if (commandContext) {
217
+ const profileRuntime = resolveHookOperatingProfile({
218
+ input: { cwd: projectRoot, session_id: commandContext.sessionId || requestedSession },
219
+ });
198
220
  const taskSnapshot = buildTaskContractSnapshot({
199
221
  vaultBase,
200
222
  projectRoot,
201
223
  changeSlug: slug,
202
224
  identity: commandContext,
225
+ profile: profileRuntime.profile,
203
226
  });
204
227
  const taskEvaluations = evaluateTaskContracts(taskSnapshot);
205
228
  const executeEvaluations = taskEvaluations.filter((task) => task.phase !== 'verify');
@@ -251,6 +274,7 @@ export function runVerify(argv) {
251
274
  }),
252
275
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
253
276
  sensors: evidence,
277
+ tddAttestations,
254
278
  };
255
279
  writeAuthority('verificacao.json', `${JSON.stringify(pkg, null, 2)}\n`);
256
280
  if (reqIds.length === 0) {