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.
- package/CHANGELOG.md +103 -0
- package/README.en.md +28 -11
- package/README.md +28 -11
- package/docs/en/commands/capabilities.md +82 -0
- package/docs/en/commands/getting-started.md +3 -1
- package/docs/en/commands/mcp.md +99 -0
- package/docs/en/commands/portable.md +88 -0
- package/docs/en/commands/sync-protocol.md +58 -0
- package/docs/en/commands/tdd.md +96 -0
- package/docs/en/commands/verify.md +5 -0
- package/docs/pt-BR/commands/capabilities.md +82 -0
- package/docs/pt-BR/commands/getting-started.md +3 -2
- package/docs/pt-BR/commands/mcp.md +99 -0
- package/docs/pt-BR/commands/portable.md +87 -0
- package/docs/pt-BR/commands/sync-protocol.md +58 -0
- package/docs/pt-BR/commands/tdd.md +96 -0
- package/docs/pt-BR/commands/verify.md +5 -0
- package/hooks/active-context-store.mjs +2 -0
- package/hooks/change-core.mjs +5 -0
- package/hooks/project-scope.mjs +2 -1
- package/hooks/session-ensure.mjs +23 -7
- package/hooks/session-start.mjs +20 -5
- package/package.json +3 -3
- package/packages/cli/src/index.mjs +42 -2
- package/packages/harness/src/sensors-core.mjs +16 -3
- package/packages/integrations/src/capabilities.mjs +220 -0
- package/packages/integrations/src/index.mjs +1 -0
- package/packages/mcp/src/audit.mjs +49 -0
- package/packages/mcp/src/cli.mjs +78 -0
- package/packages/mcp/src/config.mjs +22 -1
- package/packages/mcp/src/effects.mjs +115 -0
- package/packages/mcp/src/executor.mjs +354 -0
- package/packages/mcp/src/index.mjs +7 -0
- package/packages/mcp/src/server.mjs +342 -0
- package/packages/mcp/src/stdio.mjs +38 -0
- package/packages/mcp/src/sync.mjs +56 -0
- package/packages/pi/package.json +2 -1
- package/packages/pi/src/index.mjs +29 -0
- package/schema/handoff-contract-v1.schema.json +4 -0
- package/schema/host-capability-manifest-v1.schema.json +46 -0
- package/schema/host-coverage-v1.schema.json +55 -0
- package/schema/mcp-effect-manifest-v1.schema.json +36 -0
- package/schema/mcp-tool-input-v1.schema.json +32 -0
- package/schema/mcp-tool-result-v1.schema.json +22 -0
- package/schema/portable-active-work-v1.schema.json +38 -0
- package/schema/portable-state-v1.schema.json +36 -0
- package/schema/sync-event-v1.schema.json +25 -0
- package/schema/sync-private-envelope-v1.schema.json +16 -0
- package/schema/sync-state-v1.schema.json +18 -0
- package/schema/task-contract-v1.schema.json +2 -0
- package/schema/tdd-attestation-v1.schema.json +39 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +17 -0
- package/schema/wendkeep.sensors.schema.json +19 -0
- package/src/active-context-runtime.mjs +1 -0
- package/src/capabilities.mjs +50 -0
- package/src/doctor.mjs +28 -0
- package/src/evidence-envelope.mjs +12 -6
- package/src/host-capabilities.mjs +34 -0
- package/src/init.mjs +3 -3
- package/src/mcp.mjs +7 -0
- package/src/observer-snapshot.mjs +25 -0
- package/src/portable.mjs +558 -0
- package/src/skills-seed.mjs +26 -0
- package/src/sync-adapters.mjs +188 -0
- package/src/sync-outbox.mjs +155 -0
- package/src/sync-protocol-cli.mjs +277 -0
- package/src/sync-protocol.mjs +368 -0
- package/src/sync.mjs +8 -0
- package/src/task-contracts.mjs +67 -2
- package/src/task.mjs +5 -1
- package/src/tdd-attestation-store.mjs +98 -0
- package/src/tdd-attestation.mjs +254 -0
- package/src/tdd.mjs +198 -0
- package/src/vault-readme.mjs +4 -4
- package/src/verify.mjs +24 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { realpathSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { findProjectBinding, readProjectBinding, resolveProjectVault } from '../../../src/project-vault.mjs';
|
|
9
|
+
import { resolveOperatingProfile } from '../../../src/operating-profile.mjs';
|
|
10
|
+
import { inspectSessionContext } from '../../../src/context.mjs';
|
|
11
|
+
import { listMemoryCandidates } from '../../../src/memory.mjs';
|
|
12
|
+
import { resolveCommandActiveContext } from '../../../src/active-context-runtime.mjs';
|
|
13
|
+
import { allChangesState } from '../../../hooks/change-core.mjs';
|
|
14
|
+
import { activeContextKey } from '../../../hooks/active-context-store.mjs';
|
|
15
|
+
import { readSessionRegistry } from '../../../hooks/obsidian-common.mjs';
|
|
16
|
+
import { loadEvidenceIndex, recallEvidence } from '../../vault/src/evidence-recall.mjs';
|
|
17
|
+
import { getLocale } from '../../vault/src/locale.mjs';
|
|
18
|
+
import {
|
|
19
|
+
deriveMemoryProjection,
|
|
20
|
+
enqueueMemoryEvent,
|
|
21
|
+
projectMemoryOutbox,
|
|
22
|
+
readMemoryLedger,
|
|
23
|
+
} from '../../vault/src/memory-store.mjs';
|
|
24
|
+
import { scopeForMemoryKey } from '../../vault/src/memory-scope.mjs';
|
|
25
|
+
import { sanitizeMemoryText } from '../../vault/src/memory-schema.mjs';
|
|
26
|
+
|
|
27
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
28
|
+
const BIN = join(ROOT, 'bin', 'wendkeep.mjs');
|
|
29
|
+
const OUTPUT_LIMIT = 1_000_000;
|
|
30
|
+
|
|
31
|
+
function canonicalPath(value) {
|
|
32
|
+
const absolute = resolve(String(value || ''));
|
|
33
|
+
let physical = absolute;
|
|
34
|
+
try { physical = realpathSync.native(absolute); } catch { /* validated by binding resolution */ }
|
|
35
|
+
const normalized = physical.replaceAll('\\', '/');
|
|
36
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contextFor(args) {
|
|
40
|
+
if (!isAbsolute(String(args.project_root || ''))) {
|
|
41
|
+
throw Object.assign(new Error('project_root must be absolute'), { code: 'MCP_PROJECT_INVALID' });
|
|
42
|
+
}
|
|
43
|
+
const declaredProjectRoot = resolve(String(args.project_root));
|
|
44
|
+
const requestedWorktreeRoot = resolve(String(args.worktree_root || declaredProjectRoot));
|
|
45
|
+
if (args.worktree_root && !isAbsolute(args.worktree_root)) {
|
|
46
|
+
throw Object.assign(new Error('worktree_root must be absolute'), { code: 'MCP_WORKTREE_INVALID' });
|
|
47
|
+
}
|
|
48
|
+
const worktreeBinding = findProjectBinding(requestedWorktreeRoot);
|
|
49
|
+
if (!worktreeBinding
|
|
50
|
+
|| canonicalPath(worktreeBinding.projectRoot) !== canonicalPath(requestedWorktreeRoot)) {
|
|
51
|
+
throw Object.assign(new Error('worktree_root must identify a bound project root'), {
|
|
52
|
+
code: 'MCP_WORKTREE_INVALID',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const resolution = resolveProjectVault({ startDir: worktreeBinding.projectRoot });
|
|
56
|
+
const declaredProject = canonicalPath(declaredProjectRoot);
|
|
57
|
+
const resolvedProject = canonicalPath(resolution.projectRoot || worktreeBinding.projectRoot);
|
|
58
|
+
const boundWorktree = canonicalPath(worktreeBinding.projectRoot);
|
|
59
|
+
if (declaredProject !== resolvedProject && declaredProject !== boundWorktree) {
|
|
60
|
+
throw Object.assign(new Error('declared project does not match the resolved binding'), {
|
|
61
|
+
code: 'MCP_PROJECT_SCOPE_MISMATCH',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
projectRoot: resolution.projectRoot || worktreeBinding.projectRoot,
|
|
66
|
+
worktreeRoot: worktreeBinding.projectRoot,
|
|
67
|
+
resolution,
|
|
68
|
+
vaultBase: resolution.base,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sanitize(value, key = '') {
|
|
73
|
+
if (Array.isArray(value)) return value.map((item) => sanitize(item));
|
|
74
|
+
if (value && typeof value === 'object') {
|
|
75
|
+
return Object.fromEntries(Object.entries(value)
|
|
76
|
+
.filter(([field]) => !/(?:absolute_)?path$/i.test(field))
|
|
77
|
+
.map(([field, child]) => [field, sanitize(child, field)]));
|
|
78
|
+
}
|
|
79
|
+
if (typeof value !== 'string') return value;
|
|
80
|
+
if (isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value)) return '[LOCAL_PATH]';
|
|
81
|
+
return value
|
|
82
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{12,}\b/g, '[REDACTED_SECRET]')
|
|
83
|
+
.replace(/\b[A-Za-z]:\\+[^\s)'"\r\n]+/g, '[LOCAL_PATH]')
|
|
84
|
+
.replace(/\/(?:Users|home|private|tmp)\/[^\s)'"\r\n]+/g, '[LOCAL_PATH]')
|
|
85
|
+
.slice(0, key === 'output' ? 20_000 : 4_000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseCliOutput(stdout) {
|
|
89
|
+
const text = String(stdout || '').trim();
|
|
90
|
+
if (!text) return { ok: true };
|
|
91
|
+
try { return JSON.parse(text); }
|
|
92
|
+
catch { return { ok: true, output: text }; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function runCli(argv, cwd, signal, allowedExitCodes = [0]) {
|
|
96
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
97
|
+
const child = spawn(process.execPath, [BIN, ...argv], {
|
|
98
|
+
cwd,
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
101
|
+
env: { ...process.env, OBSIDIAN_VAULT_PATH: '' },
|
|
102
|
+
});
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
const append = (current, chunk) => `${current}${chunk}`.slice(-OUTPUT_LIMIT);
|
|
106
|
+
child.stdout.setEncoding('utf8');
|
|
107
|
+
child.stderr.setEncoding('utf8');
|
|
108
|
+
child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk); });
|
|
109
|
+
child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk); });
|
|
110
|
+
const abort = () => child.kill();
|
|
111
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
112
|
+
child.once('error', rejectRun);
|
|
113
|
+
child.once('exit', (code) => {
|
|
114
|
+
signal?.removeEventListener('abort', abort);
|
|
115
|
+
const parsed = parseCliOutput(stdout);
|
|
116
|
+
if (allowedExitCodes.includes(code)) resolveRun(sanitize(parsed));
|
|
117
|
+
else {
|
|
118
|
+
const error = new Error(parsed?.error || stderr || `CLI exited ${code}`);
|
|
119
|
+
error.code = parsed?.code || 'MCP_CLI_REJECTED';
|
|
120
|
+
rejectRun(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cliArgs(tool, args, ctx) {
|
|
127
|
+
const common = ['--project', ctx.worktreeRoot, '--vault', ctx.vaultBase, '--json'];
|
|
128
|
+
const session = args.session_id ? ['--session', String(args.session_id)] : [];
|
|
129
|
+
const change = args.change ? ['--change', String(args.change)] : [];
|
|
130
|
+
const task = String(args.task || '');
|
|
131
|
+
switch (tool.name) {
|
|
132
|
+
case 'wendkeep_spec_effective': return ['spec', 'effective', ...change, ...session, ...common];
|
|
133
|
+
case 'wendkeep_task_show': return ['task', 'show', task, ...change, ...session, ...common];
|
|
134
|
+
case 'wendkeep_task_evaluate': return ['task', 'evaluate', task, ...change, ...session, ...common];
|
|
135
|
+
case 'wendkeep_task_claim': return ['task', 'claim', task, ...change, ...session, ...common];
|
|
136
|
+
case 'wendkeep_task_complete': return ['change', 'done', task, ...change, ...session, ...common];
|
|
137
|
+
case 'wendkeep_context_select': return [
|
|
138
|
+
'context', 'recover', ...session, '--select', String(args.payload?.select || ''),
|
|
139
|
+
'--revision', String(args.payload?.revision || ''), '--reason', String(args.reason), ...common,
|
|
140
|
+
];
|
|
141
|
+
default: return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function changeResult(tool, args, vaultBase) {
|
|
146
|
+
const state = allChangesState(vaultBase);
|
|
147
|
+
if (tool.name === 'wendkeep_change_list') return sanitize(state.changes);
|
|
148
|
+
const change = state.changes.find((candidate) => candidate.slug === String(args.change || ''));
|
|
149
|
+
if (!change) throw Object.assign(new Error('change not found'), { code: 'MCP_CHANGE_NOT_FOUND' });
|
|
150
|
+
return sanitize(tool.name === 'wendkeep_change_status'
|
|
151
|
+
? { slug: change.slug, current: change.current, open_count: change.openCount, done_count: change.doneCount, warning: change.warning }
|
|
152
|
+
: change);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function observerQuery(args, ctx) {
|
|
156
|
+
const {
|
|
157
|
+
openObserverDatabase,
|
|
158
|
+
readSqlProjectOverview,
|
|
159
|
+
readUsageBreakdown,
|
|
160
|
+
readUsageCalls,
|
|
161
|
+
readUsageSummary,
|
|
162
|
+
searchSqlDocuments,
|
|
163
|
+
} = await import('../../../src/observer-sql-store.mjs');
|
|
164
|
+
const kind = String(args.query || 'overview').trim();
|
|
165
|
+
const filters = args.payload?.filters && typeof args.payload.filters === 'object'
|
|
166
|
+
? args.payload.filters
|
|
167
|
+
: {};
|
|
168
|
+
const dataDir = resolve(process.env.WENDKEEP_OBSERVER_DATA_DIR || join(homedir(), '.wendkeep-observer'));
|
|
169
|
+
let db;
|
|
170
|
+
try {
|
|
171
|
+
db = openObserverDatabase(dataDir);
|
|
172
|
+
switch (kind) {
|
|
173
|
+
case 'overview': return sanitize(readSqlProjectOverview(db, ctx.resolution.projectId));
|
|
174
|
+
case 'usage_summary': return sanitize(readUsageSummary(db, ctx.resolution.projectId, filters));
|
|
175
|
+
case 'usage_breakdown': return sanitize(readUsageBreakdown(db, ctx.resolution.projectId, filters));
|
|
176
|
+
case 'usage_calls': return sanitize(readUsageCalls(db, ctx.resolution.projectId, filters));
|
|
177
|
+
case 'memory_search': return sanitize(searchSqlDocuments(
|
|
178
|
+
db, ctx.resolution.projectId, String(args.payload?.text || ''),
|
|
179
|
+
));
|
|
180
|
+
default: throw Object.assign(new Error('unsupported semantic Observer query'), {
|
|
181
|
+
code: 'MCP_OBSERVER_QUERY_INVALID',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} finally {
|
|
185
|
+
db?.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assertMemory(tool, args, ctx, identity) {
|
|
190
|
+
const payload = args.payload && typeof args.payload === 'object' ? args.payload : {};
|
|
191
|
+
const memoryKey = tool.name === 'wendkeep_handoff_publish'
|
|
192
|
+
? 'handoff.latest'
|
|
193
|
+
: String(payload.memory_key || '').trim();
|
|
194
|
+
if (!memoryKey || !Object.hasOwn(payload, 'value')) {
|
|
195
|
+
throw Object.assign(new Error('payload.memory_key and payload.value are required'), {
|
|
196
|
+
code: 'MCP_MEMORY_ASSERT_INVALID',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const authority = ['candidate', 'reported'].includes(payload.authority)
|
|
200
|
+
? payload.authority
|
|
201
|
+
: 'reported';
|
|
202
|
+
const entry = readSessionRegistry(ctx.vaultBase).sessions?.[String(args.session_id)] || {};
|
|
203
|
+
const activationId = String(entry.active_activation_id || entry.activation_id || '').trim();
|
|
204
|
+
if (!activationId) {
|
|
205
|
+
throw Object.assign(new Error('active session activation is required'), {
|
|
206
|
+
code: 'MCP_SESSION_ACTIVATION_REQUIRED',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const changeSlug = String(payload.change || entry.change_slug || '').trim();
|
|
210
|
+
const event = {
|
|
211
|
+
v: 1,
|
|
212
|
+
event_id: `mcp-${createHash('sha256').update(JSON.stringify([
|
|
213
|
+
identity.projectId, args.session_id, args.lease?.id, tool.name,
|
|
214
|
+
])).digest('hex').slice(0, 32)}`,
|
|
215
|
+
project_id: identity.projectId,
|
|
216
|
+
memory_key: memoryKey,
|
|
217
|
+
scope: scopeForMemoryKey(memoryKey, {
|
|
218
|
+
projectId: identity.projectId,
|
|
219
|
+
repositoryId: identity.repositoryId,
|
|
220
|
+
worktreeId: identity.worktreeId,
|
|
221
|
+
workSessionId: identity.workSessionId,
|
|
222
|
+
changeSlug,
|
|
223
|
+
}),
|
|
224
|
+
operation: 'assert',
|
|
225
|
+
value: payload.value,
|
|
226
|
+
authority,
|
|
227
|
+
canonical_session_id: String(args.session_id),
|
|
228
|
+
activation_id: activationId,
|
|
229
|
+
activation_epoch: Number(entry.activation_epoch || 0),
|
|
230
|
+
turn_sequence: Number(entry.last_turn_sequence || 0),
|
|
231
|
+
observed_at: new Date().toISOString(),
|
|
232
|
+
evidence: (Array.isArray(payload.evidence) ? payload.evidence : [])
|
|
233
|
+
.slice(0, 20)
|
|
234
|
+
.map((item) => sanitizeMemoryText(item)),
|
|
235
|
+
...(identity.workSessionId ? { work_session_id: identity.workSessionId } : {}),
|
|
236
|
+
};
|
|
237
|
+
const enqueued = enqueueMemoryEvent(ctx.vaultBase, event);
|
|
238
|
+
const projected = projectMemoryOutbox(ctx.vaultBase);
|
|
239
|
+
return sanitize({
|
|
240
|
+
schema_version: 1,
|
|
241
|
+
status: enqueued.status,
|
|
242
|
+
event_id: enqueued.eventId,
|
|
243
|
+
checkpoint: projected.checkpoint || null,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function executeNativeMcpTool(tool, args, { signal } = {}) {
|
|
248
|
+
const ctx = contextFor(args);
|
|
249
|
+
let writeIdentity = null;
|
|
250
|
+
if (tool.effect === 'write') {
|
|
251
|
+
writeIdentity = resolveCommandActiveContext({
|
|
252
|
+
vaultBase: ctx.vaultBase,
|
|
253
|
+
projectRoot: ctx.worktreeRoot,
|
|
254
|
+
sessionId: String(args.session_id || ''),
|
|
255
|
+
requireExisting: true,
|
|
256
|
+
});
|
|
257
|
+
if (!writeIdentity || activeContextKey(writeIdentity) !== String(args.active_context_id || '')) {
|
|
258
|
+
throw Object.assign(new Error('active context does not match the causal session'), {
|
|
259
|
+
code: 'MCP_ACTIVE_CONTEXT_MISMATCH',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const authorized = readSessionRegistry(ctx.vaultBase)
|
|
263
|
+
.sessions?.[String(args.session_id)]?.project_scope?.authorizedActions;
|
|
264
|
+
if (Array.isArray(authorized)
|
|
265
|
+
&& !authorized.includes(tool.capability)
|
|
266
|
+
&& !authorized.includes('tool:mutation')
|
|
267
|
+
&& !authorized.includes('*')) {
|
|
268
|
+
throw Object.assign(new Error('capability is not authorized by the causal project scope'), {
|
|
269
|
+
code: 'MCP_SCOPE_AUTH_REQUIRED',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (['wendkeep_memory_assert', 'wendkeep_handoff_publish'].includes(tool.name)) {
|
|
274
|
+
return assertMemory(tool, args, ctx, writeIdentity);
|
|
275
|
+
}
|
|
276
|
+
if (tool.name === 'wendkeep_checkpoint_create') {
|
|
277
|
+
return sanitize({
|
|
278
|
+
schema_version: 1,
|
|
279
|
+
checkpoint: projectMemoryOutbox(ctx.vaultBase).checkpoint || null,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (tool.name === 'wendkeep_observer_query') return observerQuery(args, ctx);
|
|
283
|
+
if (tool.name === 'wendkeep_project_status') {
|
|
284
|
+
const canonicalBinding = ctx.resolution.source === 'worktree-registry'
|
|
285
|
+
? readProjectBinding(ctx.projectRoot)
|
|
286
|
+
: null;
|
|
287
|
+
const profile = resolveOperatingProfile(canonicalBinding?.config || ctx.resolution.config || {});
|
|
288
|
+
return {
|
|
289
|
+
schema_version: 1,
|
|
290
|
+
project_id: ctx.resolution.projectId || '',
|
|
291
|
+
binding_source: ctx.resolution.source,
|
|
292
|
+
profile: profile.profile,
|
|
293
|
+
profile_source: profile.source,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (tool.name === 'wendkeep_context_status') {
|
|
297
|
+
return sanitize(inspectSessionContext({
|
|
298
|
+
vaultBase: ctx.vaultBase,
|
|
299
|
+
projectRoot: ctx.worktreeRoot,
|
|
300
|
+
sessionId: String(args.session_id || ''),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
if (tool.name === 'wendkeep_memory_recall') {
|
|
304
|
+
return sanitize(recallEvidence(loadEvidenceIndex(ctx.vaultBase), String(args.query || ''), {
|
|
305
|
+
topK: Math.min(Number(args.limit || 10), 100),
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
if (tool.name === 'wendkeep_memory_conflicts') {
|
|
309
|
+
return sanitize(listMemoryCandidates(ctx.vaultBase, { activeOnly: true }).candidates);
|
|
310
|
+
}
|
|
311
|
+
if (['wendkeep_change_list', 'wendkeep_change_show', 'wendkeep_change_status'].includes(tool.name)) {
|
|
312
|
+
return changeResult(tool, args, ctx.vaultBase);
|
|
313
|
+
}
|
|
314
|
+
if (tool.name === 'wendkeep_handoff_current') {
|
|
315
|
+
const ledger = readMemoryLedger(ctx.vaultBase);
|
|
316
|
+
if (ledger.errors.length) {
|
|
317
|
+
throw Object.assign(new Error('memory ledger is not valid'), { code: 'MCP_MEMORY_LEDGER_INVALID' });
|
|
318
|
+
}
|
|
319
|
+
const session = readSessionRegistry(ctx.vaultBase).sessions?.[String(args.session_id)] || {};
|
|
320
|
+
const workSessionId = String(session.work_session_id || '');
|
|
321
|
+
const scoped = ledger.events.filter((event) => (
|
|
322
|
+
event.memory_key === 'handoff.latest'
|
|
323
|
+
&& (!workSessionId || event.scope?.id === workSessionId || event.work_session_id === workSessionId)
|
|
324
|
+
));
|
|
325
|
+
const record = deriveMemoryProjection(ctx.vaultBase, scoped).records?.['handoff.latest'];
|
|
326
|
+
if (!record) throw Object.assign(new Error('current handoff not found'), { code: 'MCP_HANDOFF_NOT_FOUND' });
|
|
327
|
+
return sanitize({
|
|
328
|
+
schema_version: 1,
|
|
329
|
+
value: record.value,
|
|
330
|
+
revision: record.revision,
|
|
331
|
+
authority: record.source?.authority || '',
|
|
332
|
+
observed_at: record.source?.observed_at || '',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (tool.name === 'wendkeep_evidence_latest') {
|
|
336
|
+
const slug = String(args.change || allChangesState(ctx.vaultBase).current || '');
|
|
337
|
+
if (!slug) throw Object.assign(new Error('change is required'), { code: 'MCP_CHANGE_REQUIRED' });
|
|
338
|
+
const file = join(ctx.vaultBase, getLocale(ctx.vaultBase).folders.changes, slug, 'evidencia.json');
|
|
339
|
+
try { return sanitize(JSON.parse(readFileSync(file, 'utf8'))); }
|
|
340
|
+
catch { throw Object.assign(new Error('evidence not found'), { code: 'MCP_EVIDENCE_NOT_FOUND' }); }
|
|
341
|
+
}
|
|
342
|
+
const argv = cliArgs(tool, args, ctx);
|
|
343
|
+
if (argv) {
|
|
344
|
+
return runCli(
|
|
345
|
+
argv,
|
|
346
|
+
ctx.worktreeRoot,
|
|
347
|
+
signal,
|
|
348
|
+
tool.name === 'wendkeep_task_evaluate' ? [0, 1] : [0],
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
throw Object.assign(new Error(`${tool.name} is not available in the native adapter yet`), {
|
|
352
|
+
code: 'MCP_CAPABILITY_UNAVAILABLE',
|
|
353
|
+
});
|
|
354
|
+
}
|