ultra-builder-pro-cli 0.24.2 → 0.25.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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/adapters/claude.js +104 -0
  3. package/adapters/codex.js +363 -40
  4. package/adapters/grok.js +1254 -58
  5. package/adapters/kimi.js +682 -13
  6. package/adapters/opencode.js +132 -11
  7. package/docs/AGENT-CONTEXT.md +14 -0
  8. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  9. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  10. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  11. package/mcp-server/lib/artifact-registry.cjs +181 -51
  12. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  13. package/mcp-server/lib/change-workflow.cjs +21 -16
  14. package/mcp-server/lib/context-envelope.cjs +124 -51
  15. package/mcp-server/lib/decision-records.cjs +164 -78
  16. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  17. package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
  18. package/mcp-server/lib/managed-file-write.cjs +56 -0
  19. package/mcp-server/lib/prd-parser.cjs +2 -2
  20. package/mcp-server/lib/runtime-paths.cjs +43 -16
  21. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  22. package/mcp-server/lib/state-db.cjs +186 -8
  23. package/mcp-server/lib/state-ops.cjs +214 -31
  24. package/mcp-server/lib/task-contract.cjs +404 -0
  25. package/mcp-server/lib/task-expander.cjs +2 -2
  26. package/mcp-server/lib/task-ledger.cjs +175 -62
  27. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  28. package/mcp-server/lib/worker-packet.cjs +2 -2
  29. package/mcp-server/server.cjs +23 -4
  30. package/orchestrator/parallel-orchestrator.cjs +114 -27
  31. package/orchestrator/session-runner.cjs +25 -136
  32. package/package.json +1 -1
  33. package/skills/ultra-change/SKILL.md +6 -1
  34. package/skills/ultra-dev/SKILL.md +9 -4
  35. package/skills/ultra-plan/SKILL.md +13 -4
  36. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  37. package/spec/mcp-tools.yaml +114 -9
  38. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  39. package/spec/schemas/state-db.sql +10 -6
  40. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  41. package/spec/schemas/tasks.v4.5.schema.json +34 -12
package/adapters/kimi.js CHANGED
@@ -3,8 +3,10 @@
3
3
  /** Build and register a Kimi Code 0.26+ native Ultra Builder Pro plugin. */
4
4
 
5
5
  const fs = require('node:fs');
6
+ const crypto = require('node:crypto');
6
7
  const os = require('node:os');
7
8
  const path = require('node:path');
9
+ const { spawnSync } = require('node:child_process');
8
10
 
9
11
  const {
10
12
  copyTree,
@@ -21,6 +23,7 @@ const {
21
23
  const { parse: parseFm, serialize: serializeFm } = require('./_shared/frontmatter.cjs');
22
24
  const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
23
25
  const provenance = require('./_shared/provenance.cjs');
26
+ const { PUBLIC_TOOLS } = require('../mcp-server/lib/ultra-facade.cjs');
24
27
  const {
25
28
  CORE_PUBLIC_SKILLS,
26
29
  WORKFLOW_HOOK_FILES,
@@ -33,6 +36,262 @@ const MCP_SERVER_NAME = PLUGIN_ID;
33
36
  const PROVENANCE_FILE = 'provenance.json';
34
37
  const REGISTRY_RELATIVE = path.join('plugins', 'installed.json');
35
38
  const MANAGED_RELATIVE = path.join('plugins', 'managed', PLUGIN_ID);
39
+ const KIMI_PLUGIN_PROBE = String.raw`
40
+ 'use strict';
41
+ const { spawn } = require('node:child_process');
42
+
43
+ const kimiBin = process.argv[1];
44
+ const pluginRoot = process.argv[2];
45
+ const pluginId = process.argv[3];
46
+ const sessionCwd = process.argv[4];
47
+ const kimiHome = process.argv[5];
48
+ const probeTimeoutMs = Number(process.argv[6]);
49
+ const cleanupHttpTimeoutMs = Number(process.argv[7]);
50
+ const termWaitMs = Number(process.argv[8]);
51
+ const killWaitMs = Number(process.argv[9]);
52
+ let child = null;
53
+ let activeBaseUrl = '';
54
+ let activeHeaders = {};
55
+ let activeSessionId = '';
56
+ let cleanupPromise = null;
57
+
58
+ async function readJson(url, headers, options = {}, attempts = 1) {
59
+ let lastError;
60
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
61
+ try {
62
+ const response = await fetch(url, { ...options, headers: {
63
+ ...headers,
64
+ ...(options.headers || {}),
65
+ } });
66
+ if (!response.ok) throw new Error('HTTP ' + response.status);
67
+ return await response.json();
68
+ } catch (error) {
69
+ lastError = error;
70
+ if (attempt + 1 < attempts) {
71
+ await new Promise((resolve) => setTimeout(resolve, 100));
72
+ }
73
+ }
74
+ }
75
+ throw lastError;
76
+ }
77
+
78
+ function childExited() {
79
+ return !child || child.exitCode !== null || child.signalCode !== null;
80
+ }
81
+
82
+ async function waitForExitWithin(timeoutMs) {
83
+ if (childExited()) return true;
84
+ return await new Promise((resolve) => {
85
+ const onExit = () => {
86
+ clearTimeout(timer);
87
+ resolve(true);
88
+ };
89
+ const timer = setTimeout(() => {
90
+ child.removeListener('exit', onExit);
91
+ resolve(childExited());
92
+ }, timeoutMs);
93
+ child.once('exit', onExit);
94
+ });
95
+ }
96
+
97
+ function signalChild(signal) {
98
+ if (childExited()) return;
99
+ if (process.platform !== 'win32' && child.pid) {
100
+ try {
101
+ process.kill(-child.pid, signal);
102
+ return;
103
+ } catch (error) {
104
+ if (error.code !== 'ESRCH') throw error;
105
+ }
106
+ }
107
+ try {
108
+ child.kill(signal);
109
+ } catch (error) {
110
+ if (error.code !== 'ESRCH') throw error;
111
+ }
112
+ }
113
+
114
+ async function stopChild() {
115
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
116
+ signalChild('SIGTERM');
117
+ if (!await waitForExitWithin(termWaitMs)) {
118
+ signalChild('SIGKILL');
119
+ if (!await waitForExitWithin(killWaitMs)) {
120
+ throw new Error('Kimi probe process group did not exit after SIGKILL');
121
+ }
122
+ }
123
+ }
124
+
125
+ async function cleanup() {
126
+ if (cleanupPromise) return cleanupPromise;
127
+ cleanupPromise = (async () => {
128
+ let cleanupError = null;
129
+ if (activeBaseUrl && activeSessionId) {
130
+ try {
131
+ const archived = await readJson(
132
+ activeBaseUrl + '/api/v1/sessions/' + encodeURIComponent(activeSessionId) + ':archive',
133
+ activeHeaders,
134
+ {
135
+ method: 'POST',
136
+ headers: { 'content-type': 'application/json' },
137
+ body: '{}',
138
+ signal: AbortSignal.timeout(cleanupHttpTimeoutMs),
139
+ },
140
+ );
141
+ if (archived.code !== 0 || archived.data?.archived !== true) {
142
+ throw new Error('Kimi failed to archive probe session ' + activeSessionId);
143
+ }
144
+ activeSessionId = '';
145
+ } catch (error) {
146
+ cleanupError = error;
147
+ }
148
+ }
149
+ if (activeBaseUrl) {
150
+ try {
151
+ await fetch(activeBaseUrl + '/api/v1/shutdown', {
152
+ method: 'POST',
153
+ headers: activeHeaders,
154
+ signal: AbortSignal.timeout(cleanupHttpTimeoutMs),
155
+ });
156
+ } catch (_) {
157
+ // The server may close the socket before returning its shutdown response.
158
+ }
159
+ }
160
+ try {
161
+ await stopChild();
162
+ } catch (error) {
163
+ cleanupError ||= error;
164
+ }
165
+ if (cleanupError) {
166
+ throw new Error('KIMI_PLUGIN_PROBE_TIMEOUT: ' + cleanupError.message);
167
+ }
168
+ })();
169
+ return cleanupPromise;
170
+ }
171
+
172
+ async function main() {
173
+ let stdout = '';
174
+ let stderr = '';
175
+ child = spawn(kimiBin, [
176
+ 'web',
177
+ '--port', '0',
178
+ '--host', '127.0.0.1',
179
+ '--no-open',
180
+ '--debug-endpoints',
181
+ ], {
182
+ cwd: sessionCwd,
183
+ env: { ...process.env, KIMI_CODE_HOME: kimiHome },
184
+ detached: process.platform !== 'win32',
185
+ stdio: ['ignore', 'pipe', 'pipe'],
186
+ });
187
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
188
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
189
+
190
+ try {
191
+ let token = null;
192
+ let baseUrl = null;
193
+ for (let attempt = 0; attempt < 200 && (!token || !baseUrl); attempt += 1) {
194
+ token = stdout.match(/Token:\s+(\S+)/)?.[1] || null;
195
+ baseUrl = stdout.match(/Local:\s+(http:\/\/127\.0\.0\.1:\d+)/)?.[1] || null;
196
+ if (!token || !baseUrl) await new Promise((resolve) => setTimeout(resolve, 100));
197
+ }
198
+ if (!token) throw new Error('Kimi web did not publish a loopback bearer token');
199
+ if (!baseUrl) throw new Error('Kimi web did not publish its loopback URL');
200
+ const headers = { Authorization: 'Bearer ' + token };
201
+ activeBaseUrl = baseUrl;
202
+ activeHeaders = headers;
203
+ await readJson(baseUrl + '/api/v1/debug/channels', headers, {}, 200);
204
+ const response = await readJson(
205
+ baseUrl + '/api/v1/debug/pluginService/listPlugins',
206
+ headers,
207
+ );
208
+ if (response.code !== 0 || !Array.isArray(response.data)) {
209
+ throw new Error('Kimi pluginService.listPlugins returned an invalid response');
210
+ }
211
+ const plugin = response.data.find((entry) => entry && entry.id === pluginId);
212
+ const mcpResponse = await readJson(
213
+ baseUrl + '/api/v1/debug/pluginService/enabledMcpServers',
214
+ headers,
215
+ );
216
+ if (mcpResponse.code !== 0 || !mcpResponse.data || typeof mcpResponse.data !== 'object') {
217
+ throw new Error('Kimi pluginService.enabledMcpServers returned an invalid response');
218
+ }
219
+ const sessionResponse = await readJson(
220
+ baseUrl + '/api/v1/sessions',
221
+ headers,
222
+ {
223
+ method: 'POST',
224
+ headers: { 'content-type': 'application/json' },
225
+ body: JSON.stringify({ metadata: { cwd: sessionCwd } }),
226
+ },
227
+ );
228
+ const sessionId = sessionResponse.data?.id;
229
+ if (sessionResponse.code !== 0 || !sessionId) {
230
+ throw new Error('Kimi failed to create a probe session');
231
+ }
232
+ activeSessionId = sessionId;
233
+ const agentBase = baseUrl
234
+ + '/api/v1/debug/session/' + encodeURIComponent(sessionId)
235
+ + '/agent/main/agentMcpService/';
236
+ const initialLoad = await readJson(
237
+ agentBase + 'waitForInitialLoad',
238
+ headers,
239
+ );
240
+ if (initialLoad.code !== 0) {
241
+ throw new Error('Kimi agentMcpService.waitForInitialLoad failed');
242
+ }
243
+ const connectionResponse = await readJson(agentBase + 'list', headers);
244
+ if (connectionResponse.code !== 0 || !Array.isArray(connectionResponse.data)) {
245
+ throw new Error('Kimi agentMcpService.list returned an invalid response');
246
+ }
247
+ const toolsResponse = await readJson(
248
+ baseUrl + '/api/v1/tools?session_id=' + encodeURIComponent(sessionId),
249
+ headers,
250
+ );
251
+ if (toolsResponse.code !== 0 || !Array.isArray(toolsResponse.data?.tools)) {
252
+ throw new Error('Kimi session tool registry returned an invalid response');
253
+ }
254
+ process.stdout.write(JSON.stringify({
255
+ plugin,
256
+ mcpServers: mcpResponse.data,
257
+ mcpConnections: connectionResponse.data,
258
+ tools: toolsResponse.data.tools,
259
+ }));
260
+ } catch (error) {
261
+ const detail = stderr.trim();
262
+ throw new Error(error.message + (detail ? ': ' + detail : ''));
263
+ } finally {
264
+ await cleanup();
265
+ }
266
+ }
267
+
268
+ let handlingSignal = false;
269
+ for (const signal of ['SIGINT', 'SIGTERM']) {
270
+ process.on(signal, () => {
271
+ if (handlingSignal) return;
272
+ handlingSignal = true;
273
+ void cleanup().finally(() => process.exit(1));
274
+ });
275
+ }
276
+
277
+ let watchdogFired = false;
278
+ const watchdog = setTimeout(() => {
279
+ watchdogFired = true;
280
+ void cleanup().catch(() => {}).finally(() => {
281
+ process.stderr.write('KIMI_PLUGIN_PROBE_TIMEOUT');
282
+ process.exit(124);
283
+ });
284
+ }, probeTimeoutMs);
285
+
286
+ main().then(() => {
287
+ clearTimeout(watchdog);
288
+ }).catch((error) => {
289
+ clearTimeout(watchdog);
290
+ if (watchdogFired) return;
291
+ process.stderr.write(error.stack || error.message);
292
+ process.exitCode = 1;
293
+ });
294
+ `;
36
295
 
37
296
  function resolveTarget(ctx = {}) {
38
297
  if (ctx.configDir) return path.resolve(ctx.configDir);
@@ -54,6 +313,296 @@ function resolveRepoRoot(ctx = {}) {
54
313
  return path.resolve(ctx.repoRoot || path.join(__dirname, '..'));
55
314
  }
56
315
 
316
+ function shouldRunHostCli(ctx = {}) {
317
+ if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
318
+ return ctx.scope === 'global' && !ctx.configDir;
319
+ }
320
+
321
+ function countSkillRoots(pluginRoot) {
322
+ const root = path.join(pluginRoot, 'skills');
323
+ if (!fs.existsSync(root)) return 0;
324
+ return fs.readdirSync(root, { withFileTypes: true })
325
+ .filter((entry) => (
326
+ entry.isDirectory()
327
+ && fs.existsSync(path.join(root, entry.name, 'SKILL.md'))
328
+ ))
329
+ .length;
330
+ }
331
+
332
+ function probeKimiPlugin(ctx = {}, consumer = {}) {
333
+ const probeTimeoutMs = ctx.hostCliTimeoutMs || 45000;
334
+ const cleanupHttpTimeoutMs = Math.min(
335
+ 1000,
336
+ Math.max(10, Math.floor(probeTimeoutMs / 8)),
337
+ );
338
+ const termWaitMs = Math.min(1000, Math.max(10, Math.floor(probeTimeoutMs / 8)));
339
+ const killWaitMs = Math.min(1000, Math.max(10, Math.floor(probeTimeoutMs / 8)));
340
+ const outerTimeoutMs = probeTimeoutMs
341
+ + (cleanupHttpTimeoutMs * 2)
342
+ + termWaitMs
343
+ + killWaitMs
344
+ + 250;
345
+ const result = spawnSync(
346
+ process.execPath,
347
+ [
348
+ '-e',
349
+ KIMI_PLUGIN_PROBE,
350
+ ctx.kimiBin || 'kimi',
351
+ consumer.pluginRoot || resolvePluginRoot(ctx),
352
+ PLUGIN_ID,
353
+ path.resolve(ctx.cwd || process.cwd()),
354
+ consumer.kimiHome || resolveTarget(ctx),
355
+ String(probeTimeoutMs),
356
+ String(cleanupHttpTimeoutMs),
357
+ String(termWaitMs),
358
+ String(killWaitMs),
359
+ ],
360
+ {
361
+ encoding: 'utf8',
362
+ stdio: ['ignore', 'pipe', 'pipe'],
363
+ killSignal: 'SIGKILL',
364
+ env: {
365
+ ...process.env,
366
+ },
367
+ timeout: outerTimeoutMs,
368
+ },
369
+ );
370
+ if (result.error) {
371
+ if (result.error.code === 'ETIMEDOUT') {
372
+ const error = new Error('Kimi native plugin probe timed out');
373
+ error.code = 'HOST_CLI_TIMEOUT';
374
+ throw error;
375
+ }
376
+ throw result.error;
377
+ }
378
+ if (result.status === 124 || result.stderr.includes('KIMI_PLUGIN_PROBE_TIMEOUT')) {
379
+ const error = new Error('Kimi native plugin probe timed out');
380
+ error.code = 'HOST_CLI_TIMEOUT';
381
+ throw error;
382
+ }
383
+ if (result.status !== 0) {
384
+ const detail = (result.stderr || result.stdout || '').trim();
385
+ throw new Error(`Kimi native plugin probe failed${detail ? `: ${detail}` : ''}`);
386
+ }
387
+ const payload = JSON.parse(result.stdout);
388
+ return payload;
389
+ }
390
+
391
+ function stableHash8(input) {
392
+ let hash = 2166136261;
393
+ for (let index = 0; index < input.length; index += 1) {
394
+ hash ^= input.codePointAt(index);
395
+ hash = Math.trunc(Math.imul(hash, 16777619));
396
+ }
397
+ return hash.toString(16).padStart(8, '0');
398
+ }
399
+
400
+ function sanitizeKimiMcpNamePart(value) {
401
+ return value.replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_');
402
+ }
403
+
404
+ function matchesQualifiedKimiToolName(observed, serverId, publicName) {
405
+ if (typeof observed !== 'string') return false;
406
+ const full = `mcp__${sanitizeKimiMcpNamePart(serverId)}__${sanitizeKimiMcpNamePart(publicName)}`;
407
+ if (observed === full) return true;
408
+ const truncated = /^(.*)_(-?[0-9a-f]{8})$/.exec(observed);
409
+ return !!truncated
410
+ && truncated[2] === stableHash8(full)
411
+ && truncated[1] === full.slice(0, truncated[1].length);
412
+ }
413
+
414
+ function normalizeKimiToolIdentity(descriptor, serverId, expectedTools) {
415
+ if (typeof serverId !== 'string' || !serverId) return null;
416
+ const matches = expectedTools.filter((name) => (
417
+ matchesQualifiedKimiToolName(descriptor?.name, serverId, name)
418
+ ));
419
+ return matches.length === 1 ? matches[0] : null;
420
+ }
421
+
422
+ function sameStringSet(actual, expected) {
423
+ if (!Array.isArray(actual) || actual.length !== expected.length) return false;
424
+ const actualSet = new Set(actual);
425
+ return actualSet.size === expected.length
426
+ && expected.every((value) => actualSet.has(value));
427
+ }
428
+
429
+ function inspectActualKimiRegistration(ctx = {}) {
430
+ const registryFile = resolveRegistryFile(ctx);
431
+ const pluginRoot = resolvePluginRoot(ctx);
432
+ if (!fs.existsSync(registryFile)) {
433
+ return {
434
+ registryFile,
435
+ pluginRoot,
436
+ registryBytes: null,
437
+ record: null,
438
+ registryOk: false,
439
+ pluginRootOk: false,
440
+ };
441
+ }
442
+ const registryBytes = fs.readFileSync(registryFile);
443
+ const registry = loadRegistry(registryFile);
444
+ const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID) || null;
445
+ const registryOk = !!record
446
+ && path.resolve(record.root || '') === path.resolve(pluginRoot)
447
+ && record.source === 'local-path'
448
+ && record.enabled === true;
449
+ let pluginRootOk = false;
450
+ if (registryOk) {
451
+ try {
452
+ const stat = fs.lstatSync(pluginRoot);
453
+ pluginRootOk = stat.isDirectory()
454
+ && !stat.isSymbolicLink()
455
+ && isManaged(pluginRoot)
456
+ && fs.existsSync(path.join(pluginRoot, 'kimi.plugin.json'));
457
+ } catch (error) {
458
+ if (error.code !== 'ENOENT') throw error;
459
+ }
460
+ }
461
+ return {
462
+ registryFile,
463
+ pluginRoot,
464
+ registryBytes,
465
+ record,
466
+ registryOk,
467
+ pluginRootOk,
468
+ };
469
+ }
470
+
471
+ function createIsolatedKimiRegistry(actual) {
472
+ const kimiHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-doctor-'));
473
+ const registryFile = path.join(kimiHome, REGISTRY_RELATIVE);
474
+ ensureDir(path.dirname(registryFile));
475
+ fs.writeFileSync(registryFile, actual.registryBytes);
476
+ if (!fs.readFileSync(registryFile).equals(actual.registryBytes)) {
477
+ removeTree(kimiHome);
478
+ throw new Error('failed to create a byte-for-byte Kimi registry clone');
479
+ }
480
+ return { kimiHome, registryFile };
481
+ }
482
+
483
+ function invalidActualKimiRegistration(actual) {
484
+ const error = new Error(
485
+ `Kimi actual registration is invalid: ${actual.registryFile} -> ${actual.pluginRoot}`,
486
+ );
487
+ error.code = 'ACTUAL_KIMI_REGISTRATION_INVALID';
488
+ error.actual = actual;
489
+ return error;
490
+ }
491
+
492
+ function inspectKimiHost(ctx = {}) {
493
+ const actual = inspectActualKimiRegistration(ctx);
494
+ if (!actual.registryOk || !actual.pluginRootOk || !actual.registryBytes) {
495
+ throw invalidActualKimiRegistration(actual);
496
+ }
497
+ const target = actual.pluginRoot;
498
+ const manifest = JSON.parse(fs.readFileSync(path.join(target, 'kimi.plugin.json'), 'utf8'));
499
+ const isolated = createIsolatedKimiRegistry(actual);
500
+ try {
501
+ const doctorResult = spawnSync(ctx.kimiBin || 'kimi', ['doctor'], {
502
+ encoding: 'utf8',
503
+ timeout: ctx.hostCliTimeoutMs || 30000,
504
+ killSignal: 'SIGKILL',
505
+ stdio: ['ignore', 'pipe', 'pipe'],
506
+ env: {
507
+ ...process.env,
508
+ KIMI_CODE_HOME: isolated.kimiHome,
509
+ },
510
+ });
511
+ if (doctorResult.error) {
512
+ if (doctorResult.error.code === 'ETIMEDOUT') {
513
+ const error = new Error('kimi doctor timed out');
514
+ error.code = 'HOST_CLI_TIMEOUT';
515
+ throw error;
516
+ }
517
+ throw doctorResult.error;
518
+ }
519
+ if (doctorResult.status !== 0) {
520
+ const detail = (doctorResult.stderr || doctorResult.stdout || '').trim();
521
+ throw new Error(`kimi doctor failed${detail ? `: ${detail}` : ''}`);
522
+ }
523
+ if (!fs.readFileSync(isolated.registryFile).equals(actual.registryBytes)) {
524
+ throw new Error('isolated Kimi doctor mutated its registry clone');
525
+ }
526
+ const probe = probeKimiPlugin(ctx, {
527
+ kimiHome: isolated.kimiHome,
528
+ pluginRoot: actual.pluginRoot,
529
+ });
530
+ const plugin = probe.plugin || null;
531
+ const expected = {
532
+ skillCount: countSkillRoots(target),
533
+ mcpServerCount: Object.keys(manifest.mcpServers || {}).length,
534
+ hookCount: Array.isArray(manifest.hooks) ? manifest.hooks.length : 0,
535
+ commandCount: fs.readdirSync(path.join(target, 'commands'))
536
+ .filter((name) => name.endsWith('.md')).length,
537
+ };
538
+ const pluginOk = !!plugin
539
+ && plugin.enabled === true
540
+ && plugin.state === 'ok'
541
+ && plugin.hasErrors === false
542
+ && plugin.version === manifest.version
543
+ && plugin.skillCount === expected.skillCount
544
+ && plugin.hookCount === expected.hookCount
545
+ && plugin.commandCount === expected.commandCount;
546
+ const nativeServerName = `plugin-${PLUGIN_ID}:${MCP_SERVER_NAME}`;
547
+ const nativeMcp = probe.mcpServers?.[nativeServerName];
548
+ const mcpConnection = (probe.mcpConnections || []).find((entry) => (
549
+ entry?.name === nativeServerName
550
+ ));
551
+ const enabledTools = manifest.mcpServers?.[MCP_SERVER_NAME]?.enabledTools;
552
+ const expectedTools = [...PUBLIC_TOOLS];
553
+ const connectionToolServerId = mcpConnection?.name?.replaceAll(':', '_');
554
+ const connectedTools = (probe.tools || []).filter((entry) => (
555
+ entry?.source === 'mcp'
556
+ && entry.mcp_server_id === connectionToolServerId
557
+ && entry.active !== false
558
+ ));
559
+ const normalizedTools = connectedTools.map((entry) => (
560
+ normalizeKimiToolIdentity(entry, connectionToolServerId, expectedTools)
561
+ ));
562
+ const mcpOk = pluginOk
563
+ && plugin.mcpServerCount === expected.mcpServerCount
564
+ && plugin.enabledMcpServerCount === expected.mcpServerCount
565
+ && nativeMcp?.enabled === true
566
+ && path.resolve(nativeMcp.cwd || '') === path.resolve(target)
567
+ && nativeMcp.command === manifest.mcpServers?.[MCP_SERVER_NAME]?.command
568
+ && JSON.stringify(nativeMcp.args) === JSON.stringify(
569
+ manifest.mcpServers?.[MCP_SERVER_NAME]?.args,
570
+ )
571
+ && sameStringSet(nativeMcp.enabledTools, expectedTools)
572
+ && sameStringSet(enabledTools, expectedTools)
573
+ && mcpConnection?.status === 'connected'
574
+ && Number.isInteger(mcpConnection.toolCount)
575
+ && mcpConnection.toolCount === expectedTools.length
576
+ && normalizedTools.every((name) => name !== null)
577
+ && sameStringSet(normalizedTools, expectedTools);
578
+ return {
579
+ stdout: doctorResult.stdout,
580
+ plugin,
581
+ pluginOk,
582
+ mcpOk,
583
+ nativeMcp,
584
+ mcpConnection,
585
+ expected,
586
+ actualRegistry: {
587
+ file: actual.registryFile,
588
+ bytes: actual.registryBytes.length,
589
+ sha256: crypto.createHash('sha256').update(actual.registryBytes).digest('hex'),
590
+ },
591
+ actualPluginRoot: actual.pluginRoot,
592
+ isolatedConsumer: { kimiHome: isolated.kimiHome },
593
+ };
594
+ } finally {
595
+ const unchanged = fs.existsSync(actual.registryFile)
596
+ && fs.readFileSync(actual.registryFile).equals(actual.registryBytes);
597
+ removeTree(isolated.kimiHome);
598
+ if (!unchanged) {
599
+ const error = new Error(`Kimi doctor mutated the actual registry: ${actual.registryFile}`);
600
+ error.code = 'ACTUAL_KIMI_REGISTRY_MUTATED';
601
+ throw error;
602
+ }
603
+ }
604
+ }
605
+
57
606
  function loadRegistry(file) {
58
607
  if (!fs.existsSync(file)) return { version: 1, plugins: [] };
59
608
  let parsed;
@@ -237,12 +786,25 @@ function copyHooks(repoRoot, pluginRoot) {
237
786
  }
238
787
 
239
788
  function mcpManifest() {
789
+ const enabledTools = [...PUBLIC_TOOLS];
240
790
  if (process.platform === 'win32') {
241
- return { transport: 'stdio', command: 'node.exe', args: ['./runtime/launch.cjs'], enabled: true };
791
+ return {
792
+ transport: 'stdio',
793
+ command: 'node.exe',
794
+ args: ['./runtime/launch.cjs'],
795
+ enabled: true,
796
+ enabledTools,
797
+ };
242
798
  }
243
799
  // Kimi's built-in `node` fallback uses the host binary's ABI. Ultra bundles
244
800
  // better-sqlite3 for the Node that runs `ubp`, so use PATH Node explicitly.
245
- return { transport: 'stdio', command: 'env', args: ['node', './runtime/launch.cjs'], enabled: true };
801
+ return {
802
+ transport: 'stdio',
803
+ command: 'env',
804
+ args: ['node', './runtime/launch.cjs'],
805
+ enabled: true,
806
+ enabledTools,
807
+ };
246
808
  }
247
809
 
248
810
  function writeManifest(repoRoot, pluginRoot) {
@@ -447,12 +1009,68 @@ function doctor(ctx = {}) {
447
1009
  report.checks.plugin_manifest = { status: manifestOk ? 'pass' : 'fail' };
448
1010
  report.checks.mcp_registration = { status: mcpOk ? 'pass' : 'fail' };
449
1011
  report.checks.hook_registration = { status: hooksOk ? 'pass' : 'fail' };
1012
+ if (shouldRunHostCli(ctx)) {
1013
+ let actual = null;
1014
+ try {
1015
+ actual = inspectActualKimiRegistration(ctx);
1016
+ } catch (error) {
1017
+ addIssue(report, 'ACTUAL_REGISTRY_INVALID', {
1018
+ path: registryFile,
1019
+ message: error.message,
1020
+ });
1021
+ }
1022
+ const actualRegistryOk = !!actual?.registryOk;
1023
+ const actualPluginRootOk = actualRegistryOk && actual.pluginRootOk;
1024
+ report.checks.actual_registry = { status: actualRegistryOk ? 'pass' : 'fail' };
1025
+ report.checks.actual_plugin_root = { status: actualPluginRootOk ? 'pass' : 'fail' };
1026
+ if (!actualRegistryOk) {
1027
+ addIssue(report, 'ACTUAL_REGISTRY_INVALID', {
1028
+ path: registryFile,
1029
+ expected_root: target,
1030
+ });
1031
+ }
1032
+ if (!actualPluginRootOk) {
1033
+ addIssue(report, 'ACTUAL_PLUGIN_ROOT_INVALID', { path: target });
1034
+ }
1035
+ if (!actualRegistryOk || !actualPluginRootOk) {
1036
+ report.checks.isolated_consumer_cli = { status: 'fail' };
1037
+ report.checks.isolated_consumer_plugin = { status: 'fail' };
1038
+ report.checks.isolated_consumer_mcp = { status: 'fail' };
1039
+ return applyNativeDoctor(report, path.join(target, 'runtime'));
1040
+ }
1041
+ try {
1042
+ const host = inspectKimiHost(ctx);
1043
+ report.checks.isolated_consumer_cli = { status: 'pass' };
1044
+ report.checks.isolated_consumer_plugin = { status: host.pluginOk ? 'pass' : 'fail' };
1045
+ report.checks.isolated_consumer_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
1046
+ if (!host.pluginOk) {
1047
+ addIssue(report, 'HOST_PLUGIN_NOT_DISCOVERED', { plugin_id: PLUGIN_ID });
1048
+ }
1049
+ if (!host.mcpOk) {
1050
+ addIssue(report, 'HOST_MCP_NOT_DISCOVERED', {
1051
+ plugin_id: PLUGIN_ID,
1052
+ server: MCP_SERVER_NAME,
1053
+ });
1054
+ }
1055
+ } catch (error) {
1056
+ report.checks.isolated_consumer_cli = { status: 'fail' };
1057
+ report.checks.isolated_consumer_plugin = { status: 'fail' };
1058
+ report.checks.isolated_consumer_mcp = { status: 'fail' };
1059
+ addIssue(
1060
+ report,
1061
+ error.code === 'HOST_CLI_TIMEOUT' ? 'HOST_CLI_TIMEOUT' : 'HOST_CLI_DOCTOR_FAILED',
1062
+ { message: error.message },
1063
+ );
1064
+ }
1065
+ }
450
1066
  return applyNativeDoctor(report, path.join(target, 'runtime'));
451
1067
  }
452
1068
 
453
1069
  function uninstall(ctx = {}) {
454
1070
  const target = resolvePluginRoot(ctx);
455
1071
  const registryFile = resolveRegistryFile(ctx);
1072
+ const registryExisted = fs.existsSync(registryFile);
1073
+ const registryBytes = registryExisted ? fs.readFileSync(registryFile) : null;
456
1074
  const registry = loadRegistry(registryFile);
457
1075
  const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
458
1076
  if (record && path.resolve(record.root || '') !== path.resolve(target)) {
@@ -463,18 +1081,68 @@ function uninstall(ctx = {}) {
463
1081
  }
464
1082
 
465
1083
  const report = { target, registry: registryFile, removed: {} };
466
- if (fs.existsSync(target)) {
467
- removeTree(target);
468
- report.removed.plugin = true;
469
- }
470
- if (record) {
471
- writeAtomic(registryFile, `${JSON.stringify({
472
- version: 1,
473
- plugins: registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID),
474
- }, null, 2)}\n`);
475
- report.removed.registration = true;
1084
+ const backup = path.join(
1085
+ path.dirname(target),
1086
+ `.${PLUGIN_ID}.uninstall-backup-${crypto.randomUUID()}`,
1087
+ );
1088
+ let pluginMoved = false;
1089
+ let registryPublicationAttempted = false;
1090
+ try {
1091
+ if (fs.existsSync(target)) {
1092
+ fs.renameSync(target, backup);
1093
+ pluginMoved = true;
1094
+ }
1095
+ if (record) {
1096
+ const expectedBytes = Buffer.from(`${JSON.stringify({
1097
+ version: 1,
1098
+ plugins: registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID),
1099
+ }, null, 2)}\n`);
1100
+ registryPublicationAttempted = true;
1101
+ writeAtomic(registryFile, expectedBytes);
1102
+ const readback = fs.readFileSync(registryFile);
1103
+ if (!readback.equals(expectedBytes)) {
1104
+ throw new Error(`Kimi registry readback mismatch after uninstall: ${registryFile}`);
1105
+ }
1106
+ const verified = loadRegistry(registryFile);
1107
+ if (verified.plugins.some((entry) => entry && entry.id === PLUGIN_ID)) {
1108
+ throw new Error(`Kimi registry still contains ${PLUGIN_ID} after uninstall`);
1109
+ }
1110
+ report.removed.registration = true;
1111
+ }
1112
+ if (pluginMoved) {
1113
+ removeTree(backup);
1114
+ report.removed.plugin = true;
1115
+ }
1116
+ return report;
1117
+ } catch (error) {
1118
+ const rollbackErrors = [];
1119
+ if (registryPublicationAttempted) {
1120
+ try {
1121
+ if (registryExisted) {
1122
+ writeAtomic(registryFile, registryBytes);
1123
+ } else if (fs.existsSync(registryFile)) {
1124
+ fs.unlinkSync(registryFile);
1125
+ }
1126
+ } catch (rollbackError) {
1127
+ rollbackErrors.push(rollbackError);
1128
+ }
1129
+ }
1130
+ if (pluginMoved && fs.existsSync(backup)) {
1131
+ try {
1132
+ if (fs.existsSync(target)) removeTree(target);
1133
+ fs.renameSync(backup, target);
1134
+ } catch (rollbackError) {
1135
+ rollbackErrors.push(rollbackError);
1136
+ }
1137
+ }
1138
+ if (rollbackErrors.length > 0) {
1139
+ throw new AggregateError(
1140
+ [error, ...rollbackErrors],
1141
+ 'Kimi uninstall and rollback both failed',
1142
+ );
1143
+ }
1144
+ throw error;
476
1145
  }
477
- return report;
478
1146
  }
479
1147
 
480
1148
  module.exports = {
@@ -482,6 +1150,7 @@ module.exports = {
482
1150
  PLUGIN_ID,
483
1151
  MCP_SERVER_NAME,
484
1152
  buildHooksManifest,
1153
+ inspectKimiHost,
485
1154
  kimiTextTransform,
486
1155
  resolveTarget,
487
1156
  resolvePluginRoot,