ultra-builder-pro-cli 0.7.0 → 0.9.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 (47) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +49 -23
  3. package/adapters/_shared/codex-assets.cjs +28 -0
  4. package/adapters/_shared/handbook.cjs +9 -1
  5. package/adapters/_shared/path-rewrite.cjs +3 -0
  6. package/adapters/_shared/provenance.cjs +286 -0
  7. package/adapters/_shared/runtime-assets.cjs +2 -1
  8. package/adapters/_shared/tests/handbook.test.cjs +6 -1
  9. package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
  10. package/adapters/_shared/tests/provenance.test.cjs +133 -0
  11. package/adapters/_shared/tests/runtime-assets.test.cjs +4 -1
  12. package/adapters/claude.js +33 -0
  13. package/adapters/codex.js +87 -0
  14. package/adapters/kimi.js +642 -0
  15. package/adapters/opencode.js +66 -0
  16. package/adapters/tests/codex.test.cjs +37 -0
  17. package/adapters/tests/kimi-hook.test.cjs +122 -0
  18. package/adapters/tests/kimi.test.cjs +255 -0
  19. package/agents/debugger.md +12 -0
  20. package/bin/handbook.js +2 -2
  21. package/bin/install.js +72 -12
  22. package/bin/orchestrator.js +1 -1
  23. package/commands/ultra-change.md +3 -2
  24. package/docs/AGENT-CONTEXT.md +5 -3
  25. package/docs/ARCHITECTURE.md +40 -6
  26. package/docs/PLAN.zh-CN.md +8 -6
  27. package/docs/ROADMAP.md +4 -4
  28. package/docs/RUNTIME-COMPAT-MATRIX.md +109 -58
  29. package/docs/USER-HANDBOOK-CONTRACT.md +4 -1
  30. package/hooks/README.md +6 -2
  31. package/hooks/adapters/kimi.py +162 -0
  32. package/hooks/workflow_checkpoint.py +15 -1
  33. package/hooks/workflow_resume.py +96 -7
  34. package/mcp-server/lib/change-workflow.cjs +69 -0
  35. package/mcp-server/lib/change-workflow.test.cjs +87 -0
  36. package/mcp-server/lib/state-db.cjs +85 -2
  37. package/mcp-server/lib/state-db.test.cjs +56 -0
  38. package/mcp-server/tests/init-project.test.cjs +2 -2
  39. package/mcp-server/tests/state-db.test.cjs +3 -3
  40. package/package.json +4 -2
  41. package/skills/ultra-change/SKILL.md +12 -3
  42. package/skills/ultra-deliver/SKILL.md +3 -1
  43. package/skills/ultra-doctor/SKILL.md +10 -3
  44. package/skills/ultra-review/SKILL.md +3 -3
  45. package/spec/mcp-tools.yaml +5 -5
  46. package/spec/schemas/skill-manifest.schema.json +2 -2
  47. package/spec/schemas/state-db.sql +4 -2
@@ -0,0 +1,642 @@
1
+ 'use strict';
2
+
3
+ /** Build and register a Kimi Code 0.26+ native Ultra Builder Pro plugin. */
4
+
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const {
10
+ copyTree,
11
+ ensureDir,
12
+ isManaged,
13
+ markManaged,
14
+ removeTree,
15
+ writeAtomic,
16
+ } = require('./_shared/file-ops.cjs');
17
+ const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
18
+ const { parse: parseFm, serialize: serializeFm } = require('./_shared/frontmatter.cjs');
19
+ const provenance = require('./_shared/provenance.cjs');
20
+ const {
21
+ CORE_PUBLIC_SKILLS,
22
+ WORKFLOW_HOOK_FILES,
23
+ skillsForRuntime,
24
+ } = require('./_shared/runtime-assets.cjs');
25
+
26
+ const PLUGIN_ID = 'ultra-builder-pro';
27
+ const MCP_SERVER_NAME = PLUGIN_ID;
28
+ const PROVENANCE_FILE = 'provenance.json';
29
+ const REGISTRY_RELATIVE = path.join('plugins', 'installed.json');
30
+ const MANAGED_RELATIVE = path.join('plugins', 'managed', PLUGIN_ID);
31
+ const BOOTSTRAP_SKILL = 'using-ultra-builder-pro';
32
+
33
+ function resolveTarget(ctx = {}) {
34
+ if (ctx.configDir) return path.resolve(ctx.configDir);
35
+ if (ctx.scope === 'global') {
36
+ return path.resolve(process.env.KIMI_CODE_HOME || path.join(ctx.homeDir || os.homedir(), '.kimi-code'));
37
+ }
38
+ return path.join(ctx.cwd || process.cwd(), '.kimi-code');
39
+ }
40
+
41
+ function resolvePluginRoot(ctx = {}) {
42
+ return path.join(resolveTarget(ctx), MANAGED_RELATIVE);
43
+ }
44
+
45
+ function resolveRegistryFile(ctx = {}) {
46
+ return path.join(resolveTarget(ctx), REGISTRY_RELATIVE);
47
+ }
48
+
49
+ function resolveRepoRoot(ctx = {}) {
50
+ return path.resolve(ctx.repoRoot || path.join(__dirname, '..'));
51
+ }
52
+
53
+ function loadRegistry(file) {
54
+ if (!fs.existsSync(file)) return { version: 1, plugins: [] };
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
58
+ } catch (error) {
59
+ throw new Error(`Failed to parse ${file}: ${error.message}`);
60
+ }
61
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
62
+ || parsed.version !== 1 || !Array.isArray(parsed.plugins)) {
63
+ throw new Error(`${file} is not a valid Kimi installed.json document`);
64
+ }
65
+ return parsed;
66
+ }
67
+
68
+ function kimiTextTransform(input, assetName = '') {
69
+ let text = String(input);
70
+ text = text.replaceAll('$CLAUDE_PLUGIN_ROOT', '$KIMI_PLUGIN_ROOT');
71
+ text = text.replaceAll('~/.claude/skills', '~/.kimi-code/skills');
72
+ text = text.replaceAll('~/.claude/hooks', '~/.kimi-code/hooks');
73
+ text = text.replaceAll('~/.claude', '~/.kimi-code');
74
+ text = text.replaceAll('~/.codex', '~/.kimi-code');
75
+ text = text.replaceAll('~/.config/opencode', '~/.kimi-code');
76
+ text = text.replaceAll('CLAUDE.md', 'AGENTS.md');
77
+ text = text.replace(/@skills\/([a-z0-9-]+)\/SKILL\.md/g, '$KIMI_PLUGIN_ROOT/skills/$1/SKILL.md');
78
+
79
+ text = text.replaceAll('`TaskCreate/TaskUpdate (session-local)`', 'Kimi `TodoList` (session-local)');
80
+ text = text.replaceAll('TaskCreate/TaskUpdate (session-local)', 'Kimi `TodoList` (session-local)');
81
+ text = text.replaceAll('`TaskCreate/TaskUpdate`', 'Kimi `TodoList`');
82
+ text = text.replaceAll('TaskCreate/TaskUpdate', 'Kimi `TodoList`');
83
+ text = text.replaceAll('`TaskCreate`', 'Kimi `TodoList`');
84
+ text = text.replaceAll('`TaskUpdate`', 'Kimi `TodoList`');
85
+ text = text.replaceAll('`TaskList`', 'the current Kimi `TodoList`');
86
+ text = text.replaceAll('TaskCreate', 'Kimi TodoList item creation');
87
+ text = text.replaceAll('TaskUpdate', 'Kimi TodoList updates');
88
+ text = text.replaceAll('TaskList', 'the current Kimi TodoList');
89
+ text = text.replaceAll('TaskOutput', 'subagent transcript output');
90
+ text = text.replaceAll('Claude Task tool', 'Kimi `AgentSwarm`');
91
+ text = text.replaceAll('Task tool', 'Kimi `AgentSwarm`');
92
+ text = text.replaceAll('multiple Task calls', 'one Kimi `AgentSwarm` call with multiple independent tasks');
93
+ text = text.replaceAll('Task calls', 'Kimi `AgentSwarm` items');
94
+ text = text.replaceAll('Task call', 'Kimi `AgentSwarm` item');
95
+ text = text.replaceAll('`run_in_background: true`', 'Kimi `AgentSwarm` parallel execution');
96
+ text = text.replaceAll('run_in_background: true', 'Kimi `AgentSwarm` parallel execution');
97
+ text = text.replaceAll('AskUserQuestion', 'a direct question to the user');
98
+ text = text.replaceAll('Bash tool', 'Kimi `Bash` tool');
99
+ text = text.replaceAll('Use Bash', 'Use Kimi `Bash`');
100
+ text = text.replaceAll('Read tool', 'Kimi `Read` tool');
101
+ text = text.replaceAll('Write tool', 'Kimi `Write` tool');
102
+ text = text.replaceAll('MCP `review.run`', 'Kimi `AgentSwarm` review workers');
103
+ text = text.replaceAll('`review.run`', 'Kimi `AgentSwarm` review workers');
104
+ text = text.replaceAll('review.run', 'Kimi `AgentSwarm` review workers');
105
+ text = text.replaceAll('MCP `ask.question`', 'a direct question to the user');
106
+ text = text.replaceAll('`ask.question`', 'a direct question to the user');
107
+ text = text.replaceAll('ask.question', 'a direct question to the user');
108
+ text = text.replaceAll('Claude runtime', 'Kimi runtime');
109
+ text = text.replaceAll('Claude-only', 'Kimi-only');
110
+ text = text.replaceAll('Claude Code、OpenCode、Codex 三个 runtime', 'Claude Code、OpenCode、Codex、Kimi Code 四个 runtime');
111
+ text = text.replaceAll('Claude Code, OpenCode, and Codex', 'Claude Code, OpenCode, Codex, and Kimi Code');
112
+
113
+ if (assetName === 'codex-collab') {
114
+ text = text.replaceAll('Claude Code remains primary', 'Kimi Code remains primary');
115
+ text = text.replaceAll('one Claude Code-owned conclusion', 'one Kimi Code-owned conclusion');
116
+ }
117
+ if (assetName === 'ultra-verify') {
118
+ text = text.replaceAll('Claude Code remains primary', 'Kimi Code remains primary');
119
+ text = text.replaceAll("Claude Code's evidence-backed analysis", "Kimi Code's evidence-backed analysis");
120
+ text = text.replaceAll('Claude writes its analysis first', 'Kimi writes its analysis first');
121
+ text = text.replaceAll('Claude writes its own analysis', 'Kimi writes its own analysis');
122
+ text = text.replaceAll('claude-analysis.md', 'kimi-analysis.md');
123
+ text = text.replaceAll('one Claude Code-owned conclusion', 'one Kimi Code-owned conclusion');
124
+ }
125
+ if (assetName === 'learn') {
126
+ text = text.replaceAll(
127
+ '~/.kimi-code/skills/learned/<name>_unverified.md',
128
+ '~/.kimi-code/skills/learned-<name>-unverified/SKILL.md',
129
+ );
130
+ text = text.replaceAll(
131
+ '~/.kimi-code/skills/learned/<pattern-slug>_unverified.md',
132
+ '~/.kimi-code/skills/learned-<pattern-slug>-unverified/SKILL.md',
133
+ );
134
+ text = text.replaceAll(
135
+ '~/.kimi-code/skills/learned/<slug>_unverified.md',
136
+ '~/.kimi-code/skills/learned-<slug>-unverified/SKILL.md',
137
+ );
138
+ text = text.replaceAll('append the `_unverified` suffix to the filename', 'append `-unverified` to the skill directory name');
139
+ text = text.replaceAll('Never overwrite an existing unverified file', 'Never overwrite an existing learned skill directory');
140
+ text = text.replaceAll('remove the `_unverified` suffix', 'rename the directory to remove `-unverified` and update its frontmatter name');
141
+ text = text.replaceAll('(`pattern-slug-2_unverified.md`)', '(`learned-pattern-slug-2-unverified/`)');
142
+ }
143
+ if (assetName === 'ultra-init') {
144
+ text = text.replaceAll('调用方(Claude / CLI / SDK)', '调用方(Kimi / CLI / SDK)');
145
+ text = text.replace(
146
+ /- 如果调用方是 Claude,可在此用 Kimi `TodoList` 跟踪 Step 0–4 的 session 内进度\n (这是 runtime 的 session-local 跟踪,不走 MCP)/,
147
+ '- 在 Kimi 中,用原生 `TodoList` 跟踪 Step 0–4 的 session 内进度;这是 session-local 协调,不走 MCP',
148
+ );
149
+ text = text.replace(
150
+ /## 调用方式(按 runtime)[\s\S]*?## 输出锚点/,
151
+ [
152
+ '## Kimi 调用方式',
153
+ '',
154
+ '| Runtime | 调用形态 |',
155
+ '|---------|----------|',
156
+ '| Kimi | `/ultra-builder-pro:ultra-init [name] [type] [stack] [git]` — 原生命令拉起此 skill |',
157
+ '',
158
+ '## 输出锚点',
159
+ ].join('\n'),
160
+ );
161
+ }
162
+ if (assetName === 'ultra-review') {
163
+ text = text.replace(
164
+ '# /ultra-review - Ultra Review System',
165
+ `# /ultra-review - Ultra Review System
166
+
167
+ ## Kimi native worker contract
168
+
169
+ Use one Kimi \`AgentSwarm\` call when two or more independent reviewers are selected; use one
170
+ foreground Kimi \`Agent\` call when only one reviewer is selected. Each reviewer first reads its
171
+ matching prompt template (for example \`$KIMI_PLUGIN_ROOT/agents/review-code.md\`) and writes only
172
+ its JSON artifact under \`SESSION_PATH\`. Run the coordinator as a final foreground Kimi \`Agent\`
173
+ after the reviewer artifacts are complete. Kimi has no custom-agent plugin manifest field; these
174
+ bundled files are bounded prompt templates, not separately registered host agents.`,
175
+ );
176
+ const executionContract = [
177
+ '### Phase 3: Kimi Reviewer Execution (File-Based)',
178
+ '',
179
+ 'When two or more reviewers are selected, call Kimi `AgentSwarm` exactly once and make it the',
180
+ 'only tool call in that response. Use `subagent_type: "coder"`, a `prompt_template` containing',
181
+ '`{{item}}`, and the selected reviewer names as the `items` array. The filled prompt must tell',
182
+ 'each reviewer to:',
183
+ '',
184
+ '1. Read `$KIMI_PLUGIN_ROOT/agents/{{item}}.md` and follow that bounded worker contract.',
185
+ '2. Review only `DIFF_FILES` / `DIFF_RANGE` and write',
186
+ ' `{SESSION_PATH}/{{item}}.json` using the schema at',
187
+ ' `$KIMI_PLUGIN_ROOT/skills/ultra-review/references/unified-schema.md`.',
188
+ '3. Report at most 12 findings with confidence >= 75, ordered by severity then confidence.',
189
+ '4. Return only `Wrote N findings (P0:X P1:X P2:X P3:X) to <filepath>` after the file exists.',
190
+ '',
191
+ 'Kimi `AgentSwarm` is foreground and accepts at least two items; it has no',
192
+ '`run_in_background` field. When exactly one reviewer is selected, launch one foreground Kimi',
193
+ '`Agent` with `subagent_type: "coder"`, `run_in_background: false`, and the same file contract.',
194
+ 'The short final lines may appear in the parent context; the JSON files remain the only findings',
195
+ 'input to the coordinator.',
196
+ '',
197
+ '### Phase 4: Validate & Coordinate',
198
+ '',
199
+ '**Step 4a: validate reviewer artifacts** — after the foreground call returns, run:',
200
+ '',
201
+ '```bash',
202
+ 'python3 "$KIMI_PLUGIN_ROOT/skills/ultra-review/scripts/review_wait.py" {SESSION_PATH} agents {AGENT_COUNT}',
203
+ '```',
204
+ '',
205
+ 'Use only the valid `review-*.json` files reported by the waiter. A partial result is explicit;',
206
+ 'zero valid reviewer files skips coordination and reports the failure.',
207
+ '',
208
+ '**Step 4b: coordinate** — Launch the coordinator with one foreground Kimi `Agent` call using',
209
+ '`subagent_type: "coder"` and `run_in_background: false`. Its prompt must first read',
210
+ '`$KIMI_PLUGIN_ROOT/agents/review-coordinator.md`, then read the valid reviewer JSON files,',
211
+ 'deduplicate findings, and write `SUMMARY.md` plus `SUMMARY.json` under `SESSION_PATH`.',
212
+ '',
213
+ '**Step 4c: validate the summary** — after the coordinator returns, run:',
214
+ '',
215
+ '```bash',
216
+ 'python3 "$KIMI_PLUGIN_ROOT/skills/ultra-review/scripts/review_wait.py" {SESSION_PATH} summary',
217
+ '```',
218
+ '',
219
+ 'Read `SUMMARY.json` only after this validation succeeds.',
220
+ ].join('\n');
221
+ text = text.replace(
222
+ /### Phase 3:[\s\S]*?### Phase 5: Report to User/,
223
+ `${executionContract}\n\n### Phase 5: Report to User`,
224
+ );
225
+ text = text.replaceAll('Background Execution', 'Reviewer Execution');
226
+ text = text.replaceAll('Wait & Coordinate', 'Validate & Coordinate');
227
+ }
228
+ text = text.replace(
229
+ /(^|[\s`("'])\/ultra-(?!builder-pro:)([a-z][a-z0-9-]*)/gm,
230
+ '$1/ultra-builder-pro:ultra-$2',
231
+ );
232
+ text = text.replace(
233
+ /(^|[\s`("'])\/learn(?=$|[\s`),.;:])/gm,
234
+ '$1/ultra-builder-pro:learn',
235
+ );
236
+ text = text.replace(
237
+ /(^|[\s`("'])\/clear(?=$|[\s`),.;:])/gm,
238
+ '$1/new',
239
+ );
240
+ return text;
241
+ }
242
+
243
+ function transformSkill(buf, relPath, skillName) {
244
+ const source = buf.toString('utf8');
245
+ if (relPath.endsWith('.json')) {
246
+ const transformed = kimiTextTransform(source, skillName);
247
+ JSON.parse(transformed);
248
+ return Buffer.from(transformed, 'utf8');
249
+ }
250
+ if (!relPath.endsWith('.md')) return buf;
251
+ const { fm, body } = parseFm(source);
252
+ if (!fm) return Buffer.from(kimiTextTransform(source, skillName), 'utf8');
253
+ const nativeFm = {
254
+ name: String(fm.name || skillName),
255
+ description: kimiTextTransform(fm.description || `${skillName} workflow`, skillName)
256
+ .replace(/\s+/g, ' ')
257
+ .trim(),
258
+ };
259
+ return Buffer.from(serializeFm(nativeFm, kimiTextTransform(body, skillName)), 'utf8');
260
+ }
261
+
262
+ function transformAgent(buf, relPath) {
263
+ if (!relPath.endsWith('.md')) return buf;
264
+ const source = buf.toString('utf8');
265
+ const { fm, body } = parseFm(source);
266
+ const name = path.basename(relPath, '.md');
267
+ if (!fm) return Buffer.from(kimiTextTransform(source, name), 'utf8');
268
+ return Buffer.from(serializeFm({
269
+ name: String(fm.name || name),
270
+ description: kimiTextTransform(fm.description || `${name} worker`, name)
271
+ .replace(/\s+/g, ' ')
272
+ .trim(),
273
+ }, kimiTextTransform(body, name)), 'utf8');
274
+ }
275
+
276
+ function copySkills(repoRoot, pluginRoot) {
277
+ const installed = [];
278
+ for (const name of skillsForRuntime('kimi')) {
279
+ const source = path.join(repoRoot, 'skills', name);
280
+ if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
281
+ throw new Error(`missing allowlisted Kimi skill: ${name}`);
282
+ }
283
+ copyTree(source, path.join(pluginRoot, 'skills', name), {
284
+ transform: (buf, relPath) => transformSkill(buf, relPath, name),
285
+ });
286
+ installed.push(name);
287
+ }
288
+ const bootstrapRoot = path.join(pluginRoot, 'skills', BOOTSTRAP_SKILL);
289
+ ensureDir(bootstrapRoot);
290
+ writeAtomic(path.join(bootstrapRoot, 'SKILL.md'), `---
291
+ name: ${BOOTSTRAP_SKILL}
292
+ description: Bootstrap the Kimi-native Ultra Builder Pro runtime contract at session start.
293
+ ---
294
+
295
+ # Ultra Builder Pro for Kimi Code
296
+
297
+ Kimi Code is the primary host and owns all edits, evidence, and final verification. Use the
298
+ namespaced \`/ultra-builder-pro:ultra-*\` commands or invoke the registered Ultra skills directly.
299
+
300
+ - \`.ultra/state.db\` is the only durable Ultra authority. JSON and Markdown under \`.ultra/\` are
301
+ projections or workflow artifacts and must never replace MCP state writes.
302
+ - Memory and code graph data belong to separately installed external providers. Ultra stores only
303
+ provider metadata references and never captures prompts, transcripts, or cross-session memory.
304
+ - Use Kimi \`TodoList\` for session coordination and Kimi \`Agent\` / \`AgentSwarm\` for bounded
305
+ workers. Bundled files under \`$KIMI_PLUGIN_ROOT/agents/\` are worker prompt templates.
306
+ - Before compaction, Ultra saves its workflow checkpoint. After compaction or recovery, inspect
307
+ \`.ultra/runtime/checkpoint.json\` and call the Ultra status/doctor MCP tools before continuing.
308
+ - If no Ultra workflow is active, the lifecycle hooks remain advisory except projection protection.
309
+ `);
310
+ installed.push(BOOTSTRAP_SKILL);
311
+ return installed;
312
+ }
313
+
314
+ function commandDescription(repoRoot, name) {
315
+ const source = path.join(repoRoot, 'commands', `${name}.md`);
316
+ if (fs.existsSync(source)) {
317
+ const { fm } = parseFm(fs.readFileSync(source, 'utf8'));
318
+ if (fm && typeof fm.description === 'string' && fm.description.trim()) {
319
+ return kimiTextTransform(fm.description, name).replace(/\s+/g, ' ').trim();
320
+ }
321
+ }
322
+ return `Run the ${name} Ultra Builder Pro workflow.`;
323
+ }
324
+
325
+ function copyCommands(repoRoot, pluginRoot) {
326
+ const root = path.join(pluginRoot, 'commands');
327
+ ensureDir(root);
328
+ for (const name of CORE_PUBLIC_SKILLS) {
329
+ writeAtomic(path.join(root, `${name}.md`), `---
330
+ description: ${JSON.stringify(commandDescription(repoRoot, name))}
331
+ ---
332
+
333
+ Use the registered \`${name}\` skill now and follow its complete workflow. Treat the current Kimi
334
+ Code session as the primary host; use native Kimi tools and the bundled Ultra MCP. Do not mutate
335
+ generated Ultra projections directly.
336
+
337
+ Arguments: $ARGUMENTS
338
+ `);
339
+ }
340
+ return CORE_PUBLIC_SKILLS.map((name) => `${name}.md`);
341
+ }
342
+
343
+ function copyAgents(repoRoot, pluginRoot) {
344
+ return copyTree(path.join(repoRoot, 'agents'), path.join(pluginRoot, 'agents'), {
345
+ transform: transformAgent,
346
+ });
347
+ }
348
+
349
+ function hookCommand(feature, ...args) {
350
+ return [
351
+ 'python3 ./hooks/adapters/kimi.py',
352
+ JSON.stringify(feature),
353
+ ...args.map((arg) => JSON.stringify(arg)),
354
+ ].join(' ');
355
+ }
356
+
357
+ function buildHooksManifest() {
358
+ return [
359
+ { event: 'SessionStart', command: hookCommand('health_check.py'), timeout: 5 },
360
+ { event: 'SessionStart', command: hookCommand('workflow_context.py'), timeout: 10 },
361
+ { event: 'PreToolUse', matcher: 'Edit|Write', command: hookCommand('active_task_context.py'), timeout: 5 },
362
+ { event: 'PreCompact', command: hookCommand('workflow_checkpoint.py'), timeout: 10 },
363
+ { event: 'PostCompact', command: hookCommand('workflow_resume.py'), timeout: 10 },
364
+ { event: 'Stop', command: hookCommand('pre_stop_check.py'), timeout: 5 },
365
+ { event: 'SubagentStart', command: hookCommand('subagent_tracker.py', 'start'), timeout: 5 },
366
+ { event: 'SubagentStop', command: hookCommand('subagent_tracker.py', 'stop'), timeout: 5 },
367
+ ];
368
+ }
369
+
370
+ function copyHooks(repoRoot, pluginRoot) {
371
+ const root = path.join(pluginRoot, 'hooks');
372
+ ensureDir(root);
373
+ for (const name of WORKFLOW_HOOK_FILES) {
374
+ const source = path.join(repoRoot, 'hooks', name);
375
+ if (!fs.existsSync(source)) throw new Error(`missing allowlisted Kimi hook: ${name}`);
376
+ fs.copyFileSync(source, path.join(root, name));
377
+ }
378
+ ensureDir(path.join(root, 'adapters'));
379
+ fs.copyFileSync(
380
+ path.join(repoRoot, 'hooks', 'adapters', 'kimi.py'),
381
+ path.join(root, 'adapters', 'kimi.py'),
382
+ );
383
+ return [...WORKFLOW_HOOK_FILES, path.join('adapters', 'kimi.py')];
384
+ }
385
+
386
+ function mcpManifest() {
387
+ if (process.platform === 'win32') {
388
+ return { transport: 'stdio', command: 'node.exe', args: ['./runtime/launch.cjs'], enabled: true };
389
+ }
390
+ // Kimi's built-in `node` fallback uses the host binary's ABI. Ultra bundles
391
+ // better-sqlite3 for the Node that runs `ubp`, so use PATH Node explicitly.
392
+ return { transport: 'stdio', command: 'env', args: ['node', './runtime/launch.cjs'], enabled: true };
393
+ }
394
+
395
+ function writeManifest(repoRoot, pluginRoot) {
396
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
397
+ const manifest = {
398
+ name: PLUGIN_ID,
399
+ version: pkg.version,
400
+ description: 'Kimi-native Ultra Builder Pro workflows, lifecycle hooks, review workers, and project-local MCP state.',
401
+ keywords: ['kimi-code', 'skills', 'hooks', 'mcp', 'ultra-builder-pro'],
402
+ author: { name: typeof pkg.author === 'string' ? pkg.author : 'Ultra Builder Pro contributors' },
403
+ homepage: pkg.homepage,
404
+ license: pkg.license || 'MIT',
405
+ skills: ['./skills'],
406
+ sessionStart: { skill: BOOTSTRAP_SKILL },
407
+ mcpServers: { [MCP_SERVER_NAME]: mcpManifest() },
408
+ hooks: buildHooksManifest(),
409
+ commands: ['./commands'],
410
+ interface: {
411
+ displayName: 'Ultra Builder Pro',
412
+ shortDescription: 'Kimi-native engineering workflows and verification gates.',
413
+ longDescription: 'Ultra workflows, continuous-change state, review worker templates, lifecycle recovery hooks, and a project-local MCP server adapted to Kimi Code.',
414
+ developerName: typeof pkg.author === 'string' ? pkg.author : 'Ultra Builder Pro contributors',
415
+ websiteURL: pkg.homepage,
416
+ },
417
+ skillInstructions: 'Kimi Code remains primary. Use Ultra MCP for durable state, TodoList for session coordination, and Agent or AgentSwarm with bundled prompt templates for bounded workers.',
418
+ };
419
+ writeAtomic(path.join(pluginRoot, 'kimi.plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`);
420
+ return manifest;
421
+ }
422
+
423
+ function buildStaging(repoRoot, staging) {
424
+ const copied = {
425
+ commands: copyCommands(repoRoot, staging),
426
+ skills: copySkills(repoRoot, staging),
427
+ agents: copyAgents(repoRoot, staging),
428
+ hooks: copyHooks(repoRoot, staging),
429
+ };
430
+ buildMcpRuntime(repoRoot, staging, { runtime: 'kimi' });
431
+ writeManifest(repoRoot, staging);
432
+ if (fs.existsSync(path.join(repoRoot, 'LICENSE'))) {
433
+ fs.copyFileSync(path.join(repoRoot, 'LICENSE'), path.join(staging, 'LICENSE'));
434
+ }
435
+ markManaged(staging, { adapter: 'kimi', plugin: PLUGIN_ID });
436
+ return copied;
437
+ }
438
+
439
+ function upsertRegistry(registry, target, repoRoot) {
440
+ const existing = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
441
+ if (existing && path.resolve(existing.root || '') !== path.resolve(target)) {
442
+ throw new Error(`refusing to replace conflicting Kimi registration: ${existing.root}`);
443
+ }
444
+ const now = new Date().toISOString();
445
+ const record = {
446
+ id: PLUGIN_ID,
447
+ root: target,
448
+ source: 'local-path',
449
+ enabled: existing?.enabled ?? true,
450
+ installedAt: existing?.installedAt || now,
451
+ updatedAt: now,
452
+ originalSource: repoRoot,
453
+ ...(existing?.capabilities ? { capabilities: existing.capabilities } : {}),
454
+ };
455
+ return {
456
+ version: 1,
457
+ plugins: [...registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID), record],
458
+ };
459
+ }
460
+
461
+ function writePluginProvenance(repoRoot, target) {
462
+ const source = provenance.packageSource(repoRoot);
463
+ const file = path.join(target, PROVENANCE_FILE);
464
+ const manifest = provenance.writeProvenance({
465
+ file,
466
+ adapter: 'kimi',
467
+ ...source,
468
+ roots: { plugin: target },
469
+ assets: provenance.assetRefsForTree('plugin', target, {
470
+ exclude: ['.ubp-managed', PROVENANCE_FILE],
471
+ }),
472
+ contracts: {
473
+ plugin_manifest: { root: 'plugin', path: 'kimi.plugin.json' },
474
+ mcp_launcher: { root: 'plugin', path: path.join('runtime', 'launch.cjs') },
475
+ hook_adapter: { root: 'plugin', path: path.join('hooks', 'adapters', 'kimi.py') },
476
+ session_bootstrap: {
477
+ root: 'plugin', path: path.join('skills', BOOTSTRAP_SKILL, 'SKILL.md'),
478
+ },
479
+ },
480
+ });
481
+ return { file, manifest };
482
+ }
483
+
484
+ function install(ctx = {}) {
485
+ const target = resolvePluginRoot(ctx);
486
+ const registryFile = resolveRegistryFile(ctx);
487
+ const repoRoot = resolveRepoRoot(ctx);
488
+ const registry = loadRegistry(registryFile);
489
+ const existingRecord = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
490
+ if (existingRecord && path.resolve(existingRecord.root || '') !== path.resolve(target)) {
491
+ throw new Error(`refusing to replace conflicting Kimi registration: ${existingRecord.root}`);
492
+ }
493
+ if (fs.existsSync(target) && !isManaged(target)) {
494
+ throw new Error(`refusing to replace unmanaged Kimi plugin: ${target}`);
495
+ }
496
+
497
+ const managedDir = path.dirname(target);
498
+ ensureDir(managedDir);
499
+ const staging = fs.mkdtempSync(path.join(managedDir, `${PLUGIN_ID}-`));
500
+ const backup = `${staging}-previous`;
501
+ let movedPrevious = false;
502
+ let published = false;
503
+ try {
504
+ const copied = buildStaging(repoRoot, staging);
505
+ if (fs.existsSync(target)) {
506
+ fs.renameSync(target, backup);
507
+ movedPrevious = true;
508
+ }
509
+ fs.renameSync(staging, target);
510
+ published = true;
511
+ const provenanceReport = writePluginProvenance(repoRoot, target);
512
+ writeAtomic(registryFile, `${JSON.stringify(upsertRegistry(registry, target, repoRoot), null, 2)}\n`);
513
+ if (movedPrevious) removeTree(backup);
514
+ return {
515
+ target,
516
+ registry: registryFile,
517
+ copied,
518
+ provenance: provenanceReport,
519
+ reload_required: true,
520
+ };
521
+ } catch (error) {
522
+ if (published && fs.existsSync(target)) removeTree(target);
523
+ else if (fs.existsSync(staging)) removeTree(staging);
524
+ if (movedPrevious && fs.existsSync(backup)) fs.renameSync(backup, target);
525
+ throw error;
526
+ }
527
+ }
528
+
529
+ function addIssue(report, code, details = {}) {
530
+ report.issues.push({ code, ...details });
531
+ }
532
+
533
+ function doctor(ctx = {}) {
534
+ const target = resolvePluginRoot(ctx);
535
+ const registryFile = resolveRegistryFile(ctx);
536
+ const repoRoot = resolveRepoRoot(ctx);
537
+ const source = provenance.packageSource(repoRoot);
538
+ const report = provenance.inspectProvenance({
539
+ file: path.join(target, PROVENANCE_FILE),
540
+ expectedAdapter: 'kimi',
541
+ expectedPackageVersion: source.packageInfo.version,
542
+ });
543
+
544
+ let registrationOk = false;
545
+ try {
546
+ const registry = loadRegistry(registryFile);
547
+ const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
548
+ registrationOk = !!record
549
+ && path.resolve(record.root || '') === path.resolve(target)
550
+ && record.source === 'local-path'
551
+ && record.enabled === true;
552
+ } catch (error) {
553
+ addIssue(report, 'PLUGIN_REGISTRATION_INVALID', { path: registryFile, message: error.message });
554
+ }
555
+ if (!registrationOk && !report.issues.some((issue) => issue.code === 'PLUGIN_REGISTRATION_INVALID')) {
556
+ addIssue(report, 'PLUGIN_REGISTRATION_INVALID', { path: registryFile, expected_root: target });
557
+ }
558
+
559
+ let manifestOk = false;
560
+ let mcpOk = false;
561
+ let hooksOk = false;
562
+ try {
563
+ const manifest = JSON.parse(fs.readFileSync(path.join(target, 'kimi.plugin.json'), 'utf8'));
564
+ manifestOk = manifest.name === PLUGIN_ID
565
+ && Array.isArray(manifest.skills)
566
+ && manifest.skills.includes('./skills')
567
+ && Array.isArray(manifest.commands)
568
+ && manifest.commands.includes('./commands')
569
+ && manifest.sessionStart?.skill === BOOTSTRAP_SKILL;
570
+ const mcp = manifest.mcpServers?.[MCP_SERVER_NAME];
571
+ const expected = mcpManifest();
572
+ mcpOk = !!mcp
573
+ && mcp.transport === expected.transport
574
+ && mcp.command === expected.command
575
+ && JSON.stringify(mcp.args) === JSON.stringify(expected.args)
576
+ && mcp.enabled === true;
577
+ hooksOk = Array.isArray(manifest.hooks)
578
+ && buildHooksManifest().every((expectedHook) => manifest.hooks.some((hook) => (
579
+ hook.event === expectedHook.event
580
+ && hook.command === expectedHook.command
581
+ && hook.matcher === expectedHook.matcher
582
+ )));
583
+ } catch (error) {
584
+ addIssue(report, 'PLUGIN_MANIFEST_INVALID', {
585
+ path: path.join(target, 'kimi.plugin.json'), message: error.message,
586
+ });
587
+ }
588
+ if (!manifestOk && !report.issues.some((issue) => issue.code === 'PLUGIN_MANIFEST_INVALID')) {
589
+ addIssue(report, 'PLUGIN_MANIFEST_INVALID', { path: path.join(target, 'kimi.plugin.json') });
590
+ }
591
+ if (!mcpOk) addIssue(report, 'MCP_REGISTRATION_INVALID', { path: path.join(target, 'kimi.plugin.json') });
592
+ if (!hooksOk) addIssue(report, 'HOOK_REGISTRATION_INVALID', { path: path.join(target, 'kimi.plugin.json') });
593
+
594
+ report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
595
+ report.checks.plugin_manifest = { status: manifestOk ? 'pass' : 'fail' };
596
+ report.checks.mcp_registration = { status: mcpOk ? 'pass' : 'fail' };
597
+ report.checks.hook_registration = { status: hooksOk ? 'pass' : 'fail' };
598
+ if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
599
+ return report;
600
+ }
601
+
602
+ function uninstall(ctx = {}) {
603
+ const target = resolvePluginRoot(ctx);
604
+ const registryFile = resolveRegistryFile(ctx);
605
+ const registry = loadRegistry(registryFile);
606
+ const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
607
+ if (record && path.resolve(record.root || '') !== path.resolve(target)) {
608
+ throw new Error(`refusing to remove conflicting Kimi registration: ${record.root}`);
609
+ }
610
+ if (fs.existsSync(target) && !isManaged(target)) {
611
+ throw new Error(`refusing to remove unmanaged Kimi plugin: ${target}`);
612
+ }
613
+
614
+ const report = { target, registry: registryFile, removed: {} };
615
+ if (fs.existsSync(target)) {
616
+ removeTree(target);
617
+ report.removed.plugin = true;
618
+ }
619
+ if (record) {
620
+ writeAtomic(registryFile, `${JSON.stringify({
621
+ version: 1,
622
+ plugins: registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID),
623
+ }, null, 2)}\n`);
624
+ report.removed.registration = true;
625
+ }
626
+ return report;
627
+ }
628
+
629
+ module.exports = {
630
+ name: 'kimi',
631
+ PLUGIN_ID,
632
+ MCP_SERVER_NAME,
633
+ BOOTSTRAP_SKILL,
634
+ buildHooksManifest,
635
+ kimiTextTransform,
636
+ resolveTarget,
637
+ resolvePluginRoot,
638
+ resolveRegistryFile,
639
+ install,
640
+ doctor,
641
+ uninstall,
642
+ };