wendkeep 0.80.2 → 0.85.1

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 +117 -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,220 @@
1
+ export const HOST_CAPABILITIES = [
2
+ 'session.start', 'session.resume', 'session.stop', 'prompt.submit',
3
+ 'tool.pre', 'tool.post', 'tool.effect.read', 'tool.effect.write',
4
+ 'tool.effect.destructive', 'edit.attribution', 'plan.approved', 'decision.capture',
5
+ 'task.completed', 'subagent.start', 'subagent.stop', 'transcript.read', 'usage.read',
6
+ ];
7
+
8
+ export const HOST_CAPABILITY_STATES = ['native', 'adapted', 'polled', 'manual', 'unavailable'];
9
+
10
+ function matrix(defaultState, overrides = {}) {
11
+ return Object.fromEntries(HOST_CAPABILITIES.map((capability) => [
12
+ capability, overrides[capability] || defaultState,
13
+ ]));
14
+ }
15
+
16
+ const effectOverrides = {
17
+ 'tool.effect.read': 'adapted',
18
+ 'tool.effect.write': 'adapted',
19
+ 'tool.effect.destructive': 'adapted',
20
+ };
21
+
22
+ export const HOST_CAPABILITY_MANIFESTS = {
23
+ claude: {
24
+ schema_version: 1,
25
+ manifest_version: '2026-08-25',
26
+ host_id: 'claude',
27
+ label: 'Claude Code',
28
+ supported_major_versions: [1, 2],
29
+ envelope_versions: [1],
30
+ capabilities: matrix('native', {
31
+ ...effectOverrides,
32
+ 'transcript.read': 'polled',
33
+ 'usage.read': 'polled',
34
+ }),
35
+ },
36
+ codex: {
37
+ schema_version: 1,
38
+ manifest_version: '2026-08-25',
39
+ host_id: 'codex',
40
+ label: 'Codex',
41
+ supported_major_versions: [0, 1],
42
+ envelope_versions: [1],
43
+ capabilities: matrix('unavailable', {
44
+ ...effectOverrides,
45
+ 'session.start': 'native',
46
+ 'session.resume': 'adapted',
47
+ 'session.stop': 'native',
48
+ 'prompt.submit': 'native',
49
+ 'tool.pre': 'native',
50
+ 'edit.attribution': 'adapted',
51
+ 'decision.capture': 'manual',
52
+ 'subagent.start': 'native',
53
+ 'subagent.stop': 'native',
54
+ 'transcript.read': 'polled',
55
+ 'usage.read': 'polled',
56
+ }),
57
+ },
58
+ pi: {
59
+ schema_version: 1,
60
+ manifest_version: '2026-08-25',
61
+ host_id: 'pi',
62
+ label: 'Pi',
63
+ supported_major_versions: [0, 1],
64
+ envelope_versions: [1],
65
+ capabilities: matrix('unavailable', {
66
+ ...effectOverrides,
67
+ 'session.start': 'adapted',
68
+ 'session.resume': 'adapted',
69
+ 'session.stop': 'adapted',
70
+ 'prompt.submit': 'adapted',
71
+ 'tool.pre': 'adapted',
72
+ 'decision.capture': 'manual',
73
+ 'task.completed': 'manual',
74
+ 'transcript.read': 'polled',
75
+ 'usage.read': 'polled',
76
+ }),
77
+ },
78
+ 'generic-mcp': {
79
+ schema_version: 1,
80
+ manifest_version: '2026-08-25',
81
+ host_id: 'generic-mcp',
82
+ label: 'Generic MCP/CLI',
83
+ supported_major_versions: ['*'],
84
+ envelope_versions: [1],
85
+ capabilities: matrix('unavailable', {
86
+ ...effectOverrides,
87
+ 'session.start': 'manual',
88
+ 'session.resume': 'manual',
89
+ 'session.stop': 'manual',
90
+ 'prompt.submit': 'manual',
91
+ 'decision.capture': 'manual',
92
+ 'task.completed': 'manual',
93
+ }),
94
+ },
95
+ };
96
+
97
+ const EVENT_CAPABILITIES = {
98
+ SessionStart: 'session.start',
99
+ SessionResume: 'session.resume',
100
+ Stop: 'session.stop',
101
+ UserPromptSubmit: 'prompt.submit',
102
+ PreToolUse: 'tool.pre',
103
+ PostToolUse: 'tool.post',
104
+ PlanApproved: 'plan.approved',
105
+ TaskCompleted: 'task.completed',
106
+ SubagentStart: 'subagent.start',
107
+ SubagentStop: 'subagent.stop',
108
+ };
109
+
110
+ function authorityFor(state) {
111
+ if (['native', 'adapted', 'polled'].includes(state)) return 'verified';
112
+ if (state === 'manual') return 'reported';
113
+ return 'unavailable';
114
+ }
115
+
116
+ export function verifyHostCapabilityManifest(manifest) {
117
+ const errors = [];
118
+ if (manifest?.schema_version !== 1) errors.push('schema_version');
119
+ if (!/^[a-z][a-z0-9-]*$/.test(String(manifest?.host_id || ''))) errors.push('host_id');
120
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(manifest?.manifest_version || ''))) errors.push('manifest_version');
121
+ const keys = Object.keys(manifest?.capabilities || {}).sort();
122
+ if (JSON.stringify(keys) !== JSON.stringify([...HOST_CAPABILITIES].sort())) errors.push('capabilities');
123
+ for (const state of Object.values(manifest?.capabilities || {})) {
124
+ if (!HOST_CAPABILITY_STATES.includes(state)) errors.push('capability_state');
125
+ }
126
+ if (!Array.isArray(manifest?.supported_major_versions) || !manifest.supported_major_versions.length) errors.push('supported_versions');
127
+ if (!Array.isArray(manifest?.envelope_versions) || !manifest.envelope_versions.length) errors.push('envelope_versions');
128
+ return { valid: errors.length === 0, errors: [...new Set(errors)] };
129
+ }
130
+
131
+ function versionSupported(manifest, version) {
132
+ if (manifest.supported_major_versions.includes('*') || !version) return true;
133
+ const match = String(version).match(/^(\d+)(?:\.|$)/);
134
+ return Boolean(match && manifest.supported_major_versions.includes(Number(match[1])));
135
+ }
136
+
137
+ export function buildCoverageFromManifest({
138
+ hostId,
139
+ hostVersion = '',
140
+ observedAt = new Date().toISOString(),
141
+ toolEffects = null,
142
+ } = {}) {
143
+ const requested = String(hostId || '').trim().toLowerCase();
144
+ const known = Object.hasOwn(HOST_CAPABILITY_MANIFESTS, requested);
145
+ const manifest = HOST_CAPABILITY_MANIFESTS[known ? requested : 'generic-mcp'];
146
+ const supported = versionSupported(manifest, hostVersion);
147
+ const capabilities = HOST_CAPABILITIES.map((capability) => {
148
+ let state = manifest.capabilities[capability];
149
+ if (capability.startsWith('tool.effect.')) {
150
+ const effect = capability.slice('tool.effect.'.length);
151
+ if (toolEffects?.manifest_valid !== true || toolEffects?.[effect] !== true) state = 'unavailable';
152
+ }
153
+ return { capability, state, authority: authorityFor(state) };
154
+ });
155
+ const degradations = capabilities
156
+ .filter((item) => ['manual', 'unavailable'].includes(item.state))
157
+ .map((item) => ({
158
+ capability: item.capability,
159
+ state: item.state,
160
+ code: item.state === 'manual' ? 'HOST_CAPABILITY_MANUAL' : 'HOST_CAPABILITY_UNAVAILABLE',
161
+ blocking: item.state === 'unavailable',
162
+ }));
163
+ if (!known) degradations.unshift({ capability: '', state: 'manual', code: 'HOST_UNKNOWN', blocking: false });
164
+ if (!supported) degradations.unshift({ capability: '', state: 'unavailable', code: 'HOST_VERSION_UNPROVEN', blocking: true });
165
+ return {
166
+ schema_version: 1,
167
+ manifest_version: manifest.manifest_version,
168
+ host_id: manifest.host_id,
169
+ requested_host_id: requested || manifest.host_id,
170
+ host_version: String(hostVersion || ''),
171
+ version_supported: supported,
172
+ observed_at: new Date(observedAt).toISOString(),
173
+ degraded: degradations.length > 0,
174
+ capabilities,
175
+ degradations,
176
+ tool_effects: toolEffects || {
177
+ manifest_valid: false, catalog_version: '', read: false, write: false,
178
+ destructive: false, unknown: 'fail-closed',
179
+ },
180
+ };
181
+ }
182
+
183
+ function validWaiver(value, capability) {
184
+ return value?.capability === capability
185
+ && value?.authority === 'human'
186
+ && String(value?.approved_by || '').trim()
187
+ && String(value?.reason || '').trim();
188
+ }
189
+
190
+ export function evaluateHostCoverage(coverage, required = [], { waivers = [] } = {}) {
191
+ const rows = new Map((coverage?.capabilities || []).map((item) => [item.capability, item]));
192
+ const findings = [];
193
+ const waived = [];
194
+ for (const capability of [...new Set(required)]) {
195
+ const row = rows.get(capability);
196
+ if (row && row.authority === 'verified') continue;
197
+ const waiver = waivers.find((item) => validWaiver(item, capability));
198
+ if (waiver) {
199
+ waived.push({ capability, approved_by: String(waiver.approved_by), reason: String(waiver.reason) });
200
+ continue;
201
+ }
202
+ findings.push({
203
+ capability,
204
+ state: row?.state || 'unavailable',
205
+ code: row?.state === 'manual' ? 'HOST_CAPABILITY_MANUAL' : 'HOST_CAPABILITY_UNAVAILABLE',
206
+ });
207
+ }
208
+ return { ok: findings.length === 0, findings, waived };
209
+ }
210
+
211
+ export function normalizeHostEnvelope({ hostId, event, envelopeVersion } = {}) {
212
+ const manifest = HOST_CAPABILITY_MANIFESTS[String(hostId || '').toLowerCase()];
213
+ if (!manifest || !manifest.envelope_versions.includes(Number(envelopeVersion))) {
214
+ return { ok: false, code: 'HOST_ENVELOPE_UNKNOWN' };
215
+ }
216
+ const capability = EVENT_CAPABILITIES[String(event || '')];
217
+ if (!capability) return { ok: false, code: 'HOST_ENVELOPE_UNKNOWN' };
218
+ const state = manifest.capabilities[capability];
219
+ return { ok: true, capability, state, authority: authorityFor(state) };
220
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './host-hooks.mjs';
2
+ export * from './capabilities.mjs';
2
3
  export * from './hook-envelope.mjs';
3
4
  export * from './prompt-content.mjs';
4
5
  export * from './transcript-usage.mjs';
@@ -0,0 +1,49 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ import {
5
+ assertVaultPathSafe,
6
+ mkdirVaultPath,
7
+ VAULT_LOCK_BUSY,
8
+ withVaultPathLock,
9
+ writeVaultFileAtomic,
10
+ } from '../../vault/src/vault-path-safety.mjs';
11
+
12
+ const OUTCOMES = new Set(['success', 'error']);
13
+ const EFFECTS = new Set(['read', 'write', 'destructive', 'unknown']);
14
+
15
+ function safeText(value, maximum = 160) {
16
+ return String(value || '').replace(/[\r\n\t]/g, ' ').slice(0, maximum);
17
+ }
18
+
19
+ export function createMcpAuditor(vaultBase, { now = () => new Date() } = {}) {
20
+ const runtime = join(vaultBase, '.brain', 'runtime');
21
+ const path = join(runtime, 'MCP_AUDIT.jsonl');
22
+ return async function auditToolCall(event = {}) {
23
+ mkdirVaultPath(vaultBase, runtime, { label: 'runtime da auditoria MCP' });
24
+ const record = {
25
+ schema_version: 1,
26
+ observed_at: now().toISOString(),
27
+ tool: safeText(event.tool, 120),
28
+ effect: EFFECTS.has(event.effect) ? event.effect : 'unknown',
29
+ capability: safeText(event.capability, 120),
30
+ outcome: OUTCOMES.has(event.outcome) ? event.outcome : 'error',
31
+ error_code: safeText(event.error_code, 120),
32
+ duration_ms: Math.max(0, Math.min(Number(event.duration_ms) || 0, 86_400_000)),
33
+ };
34
+ const outcome = withVaultPathLock(vaultBase, path, () => {
35
+ const checked = assertVaultPathSafe(vaultBase, path, {
36
+ expectedType: 'file', label: 'ledger de auditoria MCP',
37
+ });
38
+ const previous = checked.exists ? readFileSync(checked.target, 'utf8') : '';
39
+ writeVaultFileAtomic(vaultBase, path, `${previous}${JSON.stringify(record)}\n`, 'utf8', {
40
+ label: 'ledger de auditoria MCP',
41
+ });
42
+ return record;
43
+ }, { code: 'MCP_AUDIT_LOCK_BUSY' });
44
+ if (outcome === VAULT_LOCK_BUSY) {
45
+ throw Object.assign(new Error('MCP audit ledger is busy'), { code: 'MCP_AUDIT_LOCK_BUSY' });
46
+ }
47
+ return outcome;
48
+ };
49
+ }
@@ -0,0 +1,78 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+
3
+ import { resolveProjectVault } from '../../vault/src/project-vault.mjs';
4
+ import { createMcpAuditor } from './audit.mjs';
5
+ import { renderMcpClientConfig } from './config.mjs';
6
+ import { createNativeMcpServer } from './server.mjs';
7
+ import { executeNativeMcpTool } from './executor.mjs';
8
+ import { runNativeMcpStdio } from './stdio.mjs';
9
+
10
+ function optionValue(argv, name) {
11
+ const index = argv.indexOf(name);
12
+ if (index >= 0) return argv[index + 1] || '';
13
+ return argv.find((item) => item.startsWith(`${name}=`))?.slice(name.length + 1) || '';
14
+ }
15
+
16
+ export const MCP_HELP = `wendkeep mcp <serve|config> [options]
17
+
18
+ Run the native WendKeep semantic MCP server over stdio.
19
+
20
+ Options:
21
+ --vault <path> Select the bound Vault for compatibility with project MCP config.
22
+ --timeout-ms <n> Per-tool execution timeout (default: 10000; maximum: 120000).
23
+ --client <name> Config output: generic, claude, codex, or cursor.
24
+ --help, -h Show this help.
25
+ `;
26
+
27
+ export async function runMcp(argv = []) {
28
+ const [subcommand] = argv;
29
+ if (argv.includes('--help') || argv.includes('-h')) {
30
+ process.stdout.write(MCP_HELP);
31
+ return 0;
32
+ }
33
+ if (subcommand === 'config') {
34
+ const vault = optionValue(argv, '--vault');
35
+ if (!vault) {
36
+ process.stderr.write('wendkeep mcp config: --vault is required\n');
37
+ return 2;
38
+ }
39
+ try {
40
+ process.stdout.write(renderMcpClientConfig(optionValue(argv, '--client') || 'generic', vault));
41
+ return 0;
42
+ } catch (error) {
43
+ process.stderr.write(`wendkeep mcp config: ${error.message}\n`);
44
+ return 2;
45
+ }
46
+ }
47
+ if (subcommand !== 'serve') {
48
+ process.stderr.write(`wendkeep mcp: expected "serve"\n${MCP_HELP}`);
49
+ return 2;
50
+ }
51
+ const timeoutValue = optionValue(argv, '--timeout-ms');
52
+ const timeoutMs = timeoutValue ? Number.parseInt(timeoutValue, 10) : undefined;
53
+ if (timeoutValue && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000)) {
54
+ process.stderr.write('wendkeep mcp: --timeout-ms must be an integer from 1 to 120000\n');
55
+ return 2;
56
+ }
57
+ const explicitVault = optionValue(argv, '--vault');
58
+ const vaultBase = explicitVault
59
+ ? (isAbsolute(explicitVault) ? resolve(explicitVault) : resolve(process.cwd(), explicitVault))
60
+ : '';
61
+ const auditors = new Map();
62
+ const auditToolCall = async (event, { projectRoot = '' } = {}) => {
63
+ let selectedVault = vaultBase;
64
+ if (!selectedVault && projectRoot) {
65
+ selectedVault = resolveProjectVault({ startDir: projectRoot }).base;
66
+ }
67
+ if (!selectedVault) return;
68
+ if (!auditors.has(selectedVault)) auditors.set(selectedVault, createMcpAuditor(selectedVault));
69
+ await auditors.get(selectedVault)(event);
70
+ };
71
+ const server = createNativeMcpServer({
72
+ executeTool: executeNativeMcpTool,
73
+ auditToolCall,
74
+ ...(timeoutMs ? { timeoutMs } : {}),
75
+ });
76
+ await runNativeMcpStdio({ server });
77
+ return 0;
78
+ }
@@ -4,10 +4,31 @@ export function mcpServerEntry(vaultPath) {
4
4
  return {
5
5
  type: 'stdio',
6
6
  command: 'npx',
7
- args: ['-y', '@bitbonsai/mcpvault@latest', vaultPath],
7
+ args: ['--no-install', 'wendkeep', 'mcp', 'serve', '--vault', vaultPath],
8
8
  };
9
9
  }
10
10
 
11
+ function tomlString(value) {
12
+ return JSON.stringify(String(value));
13
+ }
14
+
15
+ export function renderMcpClientConfig(client, vaultPath) {
16
+ const selected = String(client || 'generic').toLowerCase();
17
+ const entry = mcpServerEntry(vaultPath);
18
+ if (selected === 'codex') {
19
+ return [
20
+ `[mcp_servers.${MCP_SERVER_KEY}]`,
21
+ `command = ${tomlString(entry.command)}`,
22
+ `args = [${entry.args.map(tomlString).join(', ')}]`,
23
+ '',
24
+ ].join('\n');
25
+ }
26
+ if (!['generic', 'claude', 'cursor'].includes(selected)) {
27
+ throw Object.assign(new Error(`unsupported MCP client: ${client}`), { code: 'MCP_CLIENT_INVALID' });
28
+ }
29
+ return `${JSON.stringify({ mcpServers: { [MCP_SERVER_KEY]: entry } }, null, 2)}\n`;
30
+ }
31
+
11
32
  export function selectMcpServers(descriptors, skipIds = []) {
12
33
  const skipSet = new Set(skipIds);
13
34
  const servers = {};
@@ -0,0 +1,115 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const EFFECTS = new Set(['read', 'write', 'destructive']);
4
+
5
+ const TOOLS = [
6
+ ['wendkeep_project_status', 'read', 'project:status'],
7
+ ['wendkeep_context_status', 'read', 'context:status'],
8
+ ['wendkeep_memory_recall', 'read', 'memory:recall'],
9
+ ['wendkeep_memory_conflicts', 'read', 'memory:conflicts'],
10
+ ['wendkeep_change_list', 'read', 'change:list'],
11
+ ['wendkeep_change_show', 'read', 'change:show'],
12
+ ['wendkeep_change_status', 'read', 'change:status'],
13
+ ['wendkeep_spec_effective', 'read', 'spec:effective'],
14
+ ['wendkeep_task_show', 'read', 'task:show'],
15
+ ['wendkeep_task_evaluate', 'read', 'task:evaluate'],
16
+ ['wendkeep_handoff_current', 'read', 'handoff:current'],
17
+ ['wendkeep_evidence_latest', 'read', 'evidence:latest'],
18
+ ['wendkeep_observer_query', 'read', 'observer:query'],
19
+ ['wendkeep_memory_assert', 'write', 'memory:assert'],
20
+ ['wendkeep_checkpoint_create', 'write', 'checkpoint:create'],
21
+ ['wendkeep_context_select', 'write', 'context:select'],
22
+ ['wendkeep_task_claim', 'write', 'task:claim'],
23
+ ['wendkeep_task_complete', 'write', 'task:complete'],
24
+ ['wendkeep_handoff_publish', 'write', 'handoff:publish'],
25
+ ].map(([name, effect, capability]) => ({
26
+ name,
27
+ effect,
28
+ capability,
29
+ effect_version: 1,
30
+ input_schema: 'wendkeep://schema/mcp-tool-input-v1',
31
+ output_schema: 'wendkeep://schema/mcp-tool-result-v1',
32
+ }));
33
+
34
+ function manifestPayload(manifest) {
35
+ return {
36
+ schema_version: manifest?.schema_version,
37
+ catalog_version: manifest?.catalog_version,
38
+ server_aliases: manifest?.server_aliases,
39
+ tools: manifest?.tools,
40
+ };
41
+ }
42
+
43
+ function manifestIntegrity(manifest) {
44
+ return `sha256:${createHash('sha256')
45
+ .update(JSON.stringify(manifestPayload(manifest)), 'utf8')
46
+ .digest('hex')}`;
47
+ }
48
+
49
+ function deepFreeze(value) {
50
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
51
+ Object.freeze(value);
52
+ for (const child of Object.values(value)) deepFreeze(child);
53
+ return value;
54
+ }
55
+
56
+ export const MCP_EFFECT_MANIFEST = deepFreeze({
57
+ schema_version: 1,
58
+ catalog_version: '2026-08-24',
59
+ server_aliases: ['wendkeep', 'wendkeep-native'],
60
+ tools: TOOLS,
61
+ integrity: 'sha256:de96c2f3f5ae3d814440b541db778f87be4550ebd2ce9dd5ae2377f354770046',
62
+ });
63
+
64
+ export function verifyMcpEffectManifest(manifest) {
65
+ const names = new Set();
66
+ const aliases = new Set();
67
+ const errors = [];
68
+ if (manifest?.schema_version !== 1) errors.push('schema_version');
69
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(manifest?.catalog_version || ''))) errors.push('catalog_version');
70
+ if (!Array.isArray(manifest?.server_aliases) || !manifest.server_aliases.length) errors.push('server_aliases');
71
+ for (const alias of manifest?.server_aliases || []) {
72
+ if (!/^[a-z][a-z0-9-]*$/.test(alias) || aliases.has(alias)) errors.push('server_alias');
73
+ aliases.add(alias);
74
+ }
75
+ if (!Array.isArray(manifest?.tools) || !manifest.tools.length) errors.push('tools');
76
+ for (const tool of manifest?.tools || []) {
77
+ if (!/^wendkeep_[a-z0-9_]+$/.test(String(tool?.name || '')) || names.has(tool.name)) errors.push('tool_name');
78
+ names.add(tool?.name);
79
+ if (!EFFECTS.has(tool?.effect)) errors.push('tool_effect');
80
+ if (!/^[a-z]+:[a-z]+$/.test(String(tool?.capability || ''))) errors.push('tool_capability');
81
+ if (tool?.effect_version !== 1) errors.push('effect_version');
82
+ if (tool?.input_schema !== 'wendkeep://schema/mcp-tool-input-v1') errors.push('input_schema');
83
+ if (tool?.output_schema !== 'wendkeep://schema/mcp-tool-result-v1') errors.push('output_schema');
84
+ }
85
+ const expected = manifestIntegrity(manifest);
86
+ if (manifest?.integrity !== expected) errors.push('integrity');
87
+ return { valid: errors.length === 0, errors: [...new Set(errors)], expected_integrity: expected };
88
+ }
89
+
90
+ function semanticToolName(toolName, manifest) {
91
+ const name = String(toolName || '');
92
+ if (name.startsWith('wendkeep_')) return name;
93
+ for (const alias of manifest.server_aliases || []) {
94
+ const prefix = `mcp__${alias}__`;
95
+ if (name.startsWith(prefix)) return name.slice(prefix.length);
96
+ }
97
+ return '';
98
+ }
99
+
100
+ export function resolveMcpToolEffect(toolName, { manifest = MCP_EFFECT_MANIFEST } = {}) {
101
+ if (!verifyMcpEffectManifest(manifest).valid) {
102
+ return { known: false, name: '', effect: 'unknown', capability: '', effect_version: 0 };
103
+ }
104
+ const name = semanticToolName(toolName, manifest);
105
+ const tool = manifest.tools.find((candidate) => candidate.name === name);
106
+ return tool
107
+ ? {
108
+ known: true,
109
+ name: tool.name,
110
+ effect: tool.effect,
111
+ capability: tool.capability,
112
+ effect_version: tool.effect_version,
113
+ }
114
+ : { known: false, name, effect: 'unknown', capability: '', effect_version: 0 };
115
+ }