ultra-builder-pro-cli 0.8.1 → 0.10.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 (61) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +45 -26
  3. package/adapters/_shared/codex-assets.cjs +38 -0
  4. package/adapters/_shared/handbook.cjs +10 -1
  5. package/adapters/_shared/path-rewrite.cjs +3 -0
  6. package/adapters/_shared/runtime-assets.cjs +3 -1
  7. package/adapters/_shared/tests/handbook.test.cjs +9 -1
  8. package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
  9. package/adapters/_shared/tests/runtime-assets.test.cjs +5 -1
  10. package/adapters/kimi.js +642 -0
  11. package/adapters/opencode.js +46 -17
  12. package/adapters/tests/claude.test.cjs +4 -1
  13. package/adapters/tests/codex.test.cjs +4 -3
  14. package/adapters/tests/kimi-hook.test.cjs +122 -0
  15. package/adapters/tests/kimi.test.cjs +259 -0
  16. package/adapters/tests/opencode.test.cjs +46 -3
  17. package/bin/handbook.js +2 -2
  18. package/bin/install.js +9 -4
  19. package/bin/orchestrator.js +1 -1
  20. package/docs/AGENT-CONTEXT.md +39 -15
  21. package/docs/ARCHITECTURE.md +35 -9
  22. package/docs/PLAN.zh-CN.md +5 -4
  23. package/docs/ROADMAP.md +8 -7
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +110 -64
  25. package/docs/USER-HANDBOOK-CONTRACT.md +13 -3
  26. package/hooks/README.md +6 -4
  27. package/hooks/active_task_context.py +14 -0
  28. package/hooks/adapters/kimi.py +162 -0
  29. package/hooks/context_spine.py +218 -0
  30. package/hooks/health_check.py +1 -1
  31. package/hooks/workflow_context.py +16 -123
  32. package/hooks/workflow_resume.py +21 -0
  33. package/mcp-server/lib/change-workflow.cjs +89 -11
  34. package/mcp-server/lib/change-workflow.test.cjs +202 -7
  35. package/mcp-server/lib/context-spine.cjs +331 -0
  36. package/mcp-server/lib/context-spine.test.cjs +73 -0
  37. package/mcp-server/lib/spec-learning.cjs +191 -0
  38. package/mcp-server/lib/spec-learning.test.cjs +62 -0
  39. package/mcp-server/lib/state-db.cjs +149 -4
  40. package/mcp-server/lib/state-db.test.cjs +61 -0
  41. package/mcp-server/server.cjs +31 -1
  42. package/mcp-server/tests/db-ops.test.cjs +3 -3
  43. package/mcp-server/tests/init-project.test.cjs +2 -2
  44. package/mcp-server/tests/server-contract.test.cjs +74 -0
  45. package/mcp-server/tests/state-db.test.cjs +20 -3
  46. package/package.json +4 -2
  47. package/skills/learn/SKILL.md +1 -0
  48. package/skills/ultra-change/SKILL.md +91 -118
  49. package/skills/ultra-deliver/SKILL.md +93 -221
  50. package/skills/ultra-dev/SKILL.md +81 -310
  51. package/skills/ultra-doctor/SKILL.md +1 -0
  52. package/skills/ultra-init/SKILL.md +1 -0
  53. package/skills/ultra-plan/SKILL.md +72 -292
  54. package/skills/ultra-review/SKILL.md +79 -407
  55. package/skills/ultra-status/SKILL.md +55 -168
  56. package/skills/ultra-test/SKILL.md +64 -183
  57. package/skills/ultra-think/SKILL.md +1 -0
  58. package/spec/cli-protocol.md +3 -0
  59. package/spec/mcp-tools.yaml +136 -7
  60. package/spec/schemas/skill-manifest.schema.json +2 -2
  61. package/spec/schemas/state-db.sql +35 -2
@@ -336,25 +336,26 @@ function pluginSource() {
336
336
  return `// Managed by Ultra Builder Pro. Rebuild through the adapter; do not edit in place.
337
337
  import fs from "node:fs";
338
338
  import path from "node:path";
339
+ import { execFileSync } from "node:child_process";
339
340
 
340
341
  const TERMINAL = new Set(["committed", "completed", "done", "cancelled"]);
341
342
  const TASKS_PROJECTION = ".ultra/tasks/tasks.json";
342
- const PROVIDER_FIELDS = new Set(["provider", "project", "reference", "revision", "indexed_head", "status"]);
343
-
344
- function providerMetadata(value) {
345
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
346
- const clean = {};
347
- for (const [kind, ref] of Object.entries(value)) {
348
- if (!["memory", "code_graph"].includes(kind) || !ref || typeof ref !== "object" || Array.isArray(ref)) continue;
349
- clean[kind] = Object.fromEntries(Object.entries(ref).filter(([key]) => PROVIDER_FIELDS.has(key)));
343
+
344
+ function gitHead(root) {
345
+ try {
346
+ return execFileSync("git", ["rev-parse", "HEAD"], {
347
+ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
348
+ }).trim();
349
+ } catch {
350
+ return null;
350
351
  }
351
- return clean;
352
352
  }
353
353
 
354
354
  function findUltraContext(directory) {
355
355
  let current = path.resolve(directory);
356
356
  while (true) {
357
357
  const ultra = path.join(current, ".ultra");
358
+ const currentHead = gitHead(current);
358
359
  const workflowFile = path.join(ultra, "workflow-state.json");
359
360
  const statePresent = fs.existsSync(path.join(ultra, "state.db"));
360
361
  let workflow = null;
@@ -375,11 +376,36 @@ function findUltraContext(directory) {
375
376
  if (!fs.existsSync(manifestPath)) continue;
376
377
  try {
377
378
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
378
- if (manifest?.change) changes.push({
379
- ...manifest.change,
380
- providers: providerMetadata(manifest.providers),
379
+ if (manifest?.change) {
380
+ const compiledHead = manifest.git?.head ?? null;
381
+ const legacyContext = manifest.schema_version !== "2.0";
382
+ const headStale = Boolean(compiledHead && currentHead && compiledHead !== currentHead);
383
+ const blockers = Array.isArray(manifest.readiness?.blockers)
384
+ ? [...manifest.readiness.blockers]
385
+ : [];
386
+ if (headStale && !blockers.includes("CONTEXT_HEAD_STALE")) blockers.push("CONTEXT_HEAD_STALE");
387
+ if (legacyContext && !blockers.includes("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED")) {
388
+ blockers.unshift("CONTEXT_SNAPSHOT_UPGRADE_REQUIRED");
389
+ }
390
+ changes.push({
391
+ id: manifest.change.id,
392
+ kind: manifest.change.kind,
393
+ status: manifest.change.status,
394
+ role: manifest.role ?? "plan",
395
+ gate: manifest.gate ?? "alignment",
396
+ nextAction: legacyContext
397
+ ? "Recompile change.context to upgrade this legacy context snapshot."
398
+ : headStale
399
+ ? "Git HEAD changed after context compilation; recompile change.context."
400
+ : (manifest.next_action ?? "Inspect Ultra status."),
401
+ readiness: legacyContext || headStale ? "blocked" : (manifest.readiness?.status ?? "ready"),
402
+ blockers,
403
+ taskId: manifest.selected_task?.id ?? manifest.resume?.task_id ?? null,
404
+ taskStatus: manifest.selected_task?.status ?? manifest.resume?.task_status ?? null,
405
+ tokenEstimate: manifest.context?.token_estimate ?? 0,
381
406
  manifestPath: path.relative(current, manifestPath),
382
407
  });
408
+ }
383
409
  } catch (error) {
384
410
  process.stderr.write(\`[ultra-builder-pro] cannot read \${manifestPath}: \${error.message}\\n\`);
385
411
  }
@@ -410,13 +436,16 @@ function contextText(active) {
410
436
  if (active.changes.length > 0) {
411
437
  const change = active.changes[0];
412
438
  lines.push(
413
- "[Ultra continuous change]",
439
+ "[Ultra context spine]",
414
440
  \`Project: \${active.root}\`,
415
441
  \`Change: \${change.id} (\${change.kind ?? "unknown"}, \${change.status ?? "active"})\`,
416
- \`Title: \${change.title ?? "unknown"}\`,
417
- \`Intent: \${change.intent ?? "unknown"}\`,
442
+ \`Task: \${change.taskId ?? "none"} (\${change.taskStatus ?? "none"})\`,
443
+ \`Role: \${change.role}\`,
444
+ \`Gate: \${change.gate}\`,
445
+ \`Readiness: \${change.readiness}\`,
446
+ ...(change.blockers.length > 0 ? [\`Blockers: \${change.blockers.join(", ")}\`] : []),
447
+ \`Next: \${change.nextAction}\`,
418
448
  \`Context manifest: \${change.manifestPath}\`,
419
- \`External providers (metadata references only): \${JSON.stringify(change.providers)}\`,
420
449
  );
421
450
  if (active.changes.length > 1) lines.push(
422
451
  \`Active change count: \${active.changes.length}; resolve scope with change.list before mutation.\`,
@@ -429,7 +458,7 @@ function contextText(active) {
429
458
  "No active continuous change. Start daily work with the ultra-change workflow.",
430
459
  );
431
460
  }
432
- lines.push("Authority: .ultra/state.db; generated JSON/Markdown are projections or artifacts.");
461
+ lines.push("Authority: .ultra/state.db; JSON/Markdown remain projections or evidence artifacts.");
433
462
  return lines.join("\\n");
434
463
  }
435
464
 
@@ -36,7 +36,10 @@ test('install builds the Claude-native plugin from the explicit Ultra allowlist'
36
36
 
37
37
  const hooks = JSON.parse(fs.readFileSync(path.join(target, 'hooks', 'hooks.json'), 'utf8'));
38
38
  const serialized = JSON.stringify(hooks);
39
- for (const name of WORKFLOW_HOOK_FILES) assert.match(serialized, new RegExp(name.replace('.', '\\.')));
39
+ for (const name of WORKFLOW_HOOK_FILES.filter((value) => value !== 'context_spine.py')) {
40
+ assert.match(serialized, new RegExp(name.replace('.', '\\.')));
41
+ }
42
+ assert.ok(fs.existsSync(path.join(target, 'hooks', 'context_spine.py')));
40
43
  assert.doesNotMatch(serialized, /memory|recall|journal|observation_capture|user_prompt_capture|block_dangerous|post_edit_guard/);
41
44
  assert.match(serialized, /\$\{CLAUDE_PLUGIN_ROOT\}/);
42
45
  } finally {
@@ -198,9 +198,10 @@ test('plugin declares current Codex hooks and a project-local Ultra MCP server',
198
198
  'SubagentStart', 'SubagentStop',
199
199
  ].sort());
200
200
  const serializedHooks = JSON.stringify(hooks);
201
- for (const feature of WORKFLOW_HOOK_FILES) {
201
+ for (const feature of WORKFLOW_HOOK_FILES.filter((value) => value !== 'context_spine.py')) {
202
202
  assert.match(serializedHooks, new RegExp(feature.replace('.', '\\.')));
203
203
  }
204
+ assert.ok(fs.existsSync(path.join(layout.pluginRoot, 'hooks', 'context_spine.py')));
204
205
  assert.doesNotMatch(serializedHooks, /memory|recall|journal|observation_capture|user_prompt_capture|block_dangerous|post_edit_guard/);
205
206
  assert.match(serializedHooks, /\$PLUGIN_ROOT\/hooks\/adapters\/codex\.py/);
206
207
  assert.match(serializedHooks, /apply_patch/);
@@ -231,8 +232,8 @@ test('plugin declares current Codex hooks and a project-local Ultra MCP server',
231
232
  const liveSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'mcp-tools.yaml'), 'utf8'));
232
233
  const upstreamSpec = yaml.load(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'upstream-mcp-tools.yaml'), 'utf8'));
233
234
  const capabilityMap = JSON.parse(fs.readFileSync(path.join(layout.pluginRoot, 'spec', 'codex-capability-map.json'), 'utf8'));
234
- assert.equal(liveSpec.tools.length, 29);
235
- assert.equal(upstreamSpec.tools.length, 29);
235
+ assert.equal(liveSpec.tools.length, 32);
236
+ assert.equal(upstreamSpec.tools.length, 32);
236
237
  assert.deepEqual(upstreamSpec.tools.map((tool) => tool.name).sort(), liveSpec.tools.map((tool) => tool.name).sort());
237
238
  assert.deepEqual(capabilityMap.live_mcp_tools.sort(), liveSpec.tools.map((tool) => tool.name).sort());
238
239
  assert.equal(Object.keys(capabilityMap.codex_native_replacements).length, 9);
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
11
+ const ADAPTER = path.join(REPO_ROOT, 'hooks', 'adapters', 'kimi.py');
12
+
13
+ function run(feature, payload, args = []) {
14
+ return spawnSync('python3', [ADAPTER, feature, ...args], {
15
+ cwd: REPO_ROOT,
16
+ input: JSON.stringify(payload),
17
+ encoding: 'utf8',
18
+ });
19
+ }
20
+
21
+ test('Kimi hook adapter rejects non-workflow policy hooks without blocking the host', () => {
22
+ const result = run('block_dangerous_commands.py', {
23
+ session_id: 'session-1',
24
+ cwd: REPO_ROOT,
25
+ hook_event_name: 'PreToolUse',
26
+ tool_name: 'Shell',
27
+ tool_input: { command: 'git reset --hard' },
28
+ });
29
+ assert.equal(result.status, 0, result.stderr);
30
+ assert.match(JSON.parse(result.stdout).message, /refused unknown feature/);
31
+ });
32
+
33
+ test('Kimi hook adapter translates active edit context to the native message field', () => {
34
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-'));
35
+ try {
36
+ fs.mkdirSync(path.join(project, '.ultra'));
37
+ fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
38
+ command: 'ultra-dev', task_id: 'task-7', step: '4.5', status: 'review_pending',
39
+ }));
40
+ const result = run('active_task_context.py', {
41
+ session_id: 'session-2',
42
+ cwd: project,
43
+ hook_event_name: 'PreToolUse',
44
+ tool_name: 'Edit',
45
+ tool_input: { file_path: 'src/a.py' },
46
+ });
47
+ assert.equal(result.status, 0, result.stderr);
48
+ const output = JSON.parse(result.stdout);
49
+ assert.match(output.message, /task-7/);
50
+ assert.equal(output.hookSpecificOutput, undefined);
51
+ } finally {
52
+ fs.rmSync(project, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ test('Kimi hook adapter emits the native deny contract for projection writes', () => {
57
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-deny-'));
58
+ try {
59
+ fs.mkdirSync(path.join(project, '.ultra'));
60
+ fs.writeFileSync(path.join(project, '.ultra', 'workflow-state.json'), JSON.stringify({
61
+ command: 'ultra-plan', task_id: 'plan', step: 'persist', status: 'active',
62
+ }));
63
+ const result = run('active_task_context.py', {
64
+ session_id: 'session-deny',
65
+ cwd: project,
66
+ hook_event_name: 'PreToolUse',
67
+ tool_name: 'Write',
68
+ tool_input: { file_path: '.ultra/tasks/tasks.json' },
69
+ });
70
+ assert.equal(result.status, 0, result.stderr);
71
+ const output = JSON.parse(result.stdout);
72
+ assert.equal(output.hookSpecificOutput.permissionDecision, 'deny');
73
+ assert.match(output.hookSpecificOutput.permissionDecisionReason, /\.ultra\/state\.db/);
74
+ } finally {
75
+ fs.rmSync(project, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test('Kimi subagent fields are normalized for the existing lifecycle tracker', () => {
80
+ const script = [
81
+ 'import importlib.util, json, pathlib',
82
+ `p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
83
+ 's = importlib.util.spec_from_file_location("kimi_hook_adapter", p)',
84
+ 'm = importlib.util.module_from_spec(s)',
85
+ 's.loader.exec_module(m)',
86
+ 'payload = {"hook_event_name":"SubagentStart","agent_name":"review-code","session_id":"s"}',
87
+ 'print(json.dumps(m.normalize_input(payload)))',
88
+ ].join('; ');
89
+ const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
90
+ assert.equal(result.status, 0, result.stderr);
91
+ assert.deepEqual(JSON.parse(result.stdout), {
92
+ hook_event_name: 'SubagentStart',
93
+ agent_name: 'review-code',
94
+ session_id: 's',
95
+ agent_id: 'review-code',
96
+ agent_type: 'review-code',
97
+ });
98
+ });
99
+
100
+ test('Kimi stop blocks and recovery context use the exact structured-output schema', () => {
101
+ const script = [
102
+ 'import importlib.util, json, pathlib',
103
+ `p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
104
+ 's = importlib.util.spec_from_file_location("kimi_hook_adapter", p)',
105
+ 'm = importlib.util.module_from_spec(s)',
106
+ 's.loader.exec_module(m)',
107
+ 'values = [m.adapt_output({"decision":"block","reason":"finish gates"}, "Stop"), m.adapt_output({"additionalContext":"restored"}, "PostCompact")]',
108
+ 'print(json.dumps(values))',
109
+ ].join('; ');
110
+ const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
111
+ assert.equal(result.status, 0, result.stderr);
112
+ assert.deepEqual(JSON.parse(result.stdout), [
113
+ {
114
+ hookSpecificOutput: {
115
+ message: 'finish gates',
116
+ permissionDecision: 'deny',
117
+ permissionDecisionReason: 'finish gates',
118
+ },
119
+ },
120
+ { message: 'restored' },
121
+ ]);
122
+ });
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const kimi = require('../kimi.js');
10
+ const {
11
+ CORE_PUBLIC_SKILLS,
12
+ WORKFLOW_HOOK_FILES,
13
+ skillsForRuntime,
14
+ } = require('../_shared/runtime-assets.cjs');
15
+
16
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
17
+ const PLUGIN_ID = 'ultra-builder-pro';
18
+
19
+ function mkTarget() {
20
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-'));
21
+ }
22
+
23
+ function layout(home) {
24
+ return {
25
+ pluginRoot: path.join(home, 'plugins', 'managed', PLUGIN_ID),
26
+ registryFile: path.join(home, 'plugins', 'installed.json'),
27
+ };
28
+ }
29
+
30
+ function readJson(file) {
31
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
32
+ }
33
+
34
+ test('install builds and registers one Kimi-native plugin without changing config.toml', () => {
35
+ const home = mkTarget();
36
+ const { pluginRoot, registryFile } = layout(home);
37
+ const configFile = path.join(home, 'config.toml');
38
+ const userConfig = '[[hooks]]\nevent = "Stop"\ncommand = "/user/hook.sh"\n';
39
+ try {
40
+ fs.writeFileSync(configFile, userConfig);
41
+ const report = kimi.install({ configDir: home, repoRoot: REPO_ROOT });
42
+
43
+ assert.equal(report.target, pluginRoot);
44
+ assert.equal(fs.readFileSync(configFile, 'utf8'), userConfig);
45
+ assert.ok(fs.existsSync(path.join(pluginRoot, '.ubp-managed')));
46
+ assert.ok(fs.existsSync(path.join(pluginRoot, 'provenance.json')));
47
+
48
+ const registry = readJson(registryFile);
49
+ assert.equal(registry.version, 1);
50
+ assert.equal(registry.plugins.length, 1);
51
+ assert.deepEqual(registry.plugins[0], {
52
+ id: PLUGIN_ID,
53
+ root: pluginRoot,
54
+ source: 'local-path',
55
+ enabled: true,
56
+ installedAt: registry.plugins[0].installedAt,
57
+ updatedAt: registry.plugins[0].updatedAt,
58
+ originalSource: REPO_ROOT,
59
+ });
60
+ } finally {
61
+ fs.rmSync(home, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test('Kimi manifest exposes native skills, commands, hooks, session bootstrap, and MCP', () => {
66
+ const home = mkTarget();
67
+ const { pluginRoot } = layout(home);
68
+ try {
69
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
70
+ const manifest = readJson(path.join(pluginRoot, 'kimi.plugin.json'));
71
+
72
+ assert.equal(manifest.name, PLUGIN_ID);
73
+ assert.deepEqual(manifest.skills, ['./skills']);
74
+ assert.deepEqual(manifest.commands, ['./commands']);
75
+ assert.deepEqual(manifest.sessionStart, { skill: 'using-ultra-builder-pro' });
76
+ assert.equal(manifest.agents, undefined, 'Kimi 0.26/0.27 has no custom-agent manifest field');
77
+ assert.deepEqual(manifest.mcpServers[PLUGIN_ID], {
78
+ transport: 'stdio',
79
+ command: process.platform === 'win32' ? 'node.exe' : 'env',
80
+ args: process.platform === 'win32'
81
+ ? ['./runtime/launch.cjs']
82
+ : ['node', './runtime/launch.cjs'],
83
+ enabled: true,
84
+ });
85
+
86
+ const hookText = JSON.stringify(manifest.hooks);
87
+ assert.match(hookText, /hooks\/adapters\/kimi\.py/);
88
+ for (const hook of WORKFLOW_HOOK_FILES.filter((value) => value !== 'context_spine.py')) {
89
+ assert.match(hookText, new RegExp(hook.replace('.', '\\.')));
90
+ }
91
+ assert.ok(fs.existsSync(path.join(pluginRoot, 'hooks', 'context_spine.py')));
92
+ assert.doesNotMatch(hookText, /memory|recall|journal|prompt[_ -]?capture|block_dangerous|post_edit_guard/i);
93
+
94
+ const commands = fs.readdirSync(path.join(pluginRoot, 'commands')).sort();
95
+ assert.deepEqual(commands, CORE_PUBLIC_SKILLS.map((name) => `${name}.md`).sort());
96
+ const commandText = fs.readFileSync(path.join(pluginRoot, 'commands', 'ultra-init.md'), 'utf8');
97
+ assert.match(commandText, /Use the registered `ultra-init` skill/);
98
+ assert.match(commandText, /\$ARGUMENTS/);
99
+ } finally {
100
+ fs.rmSync(home, { recursive: true, force: true });
101
+ }
102
+ });
103
+
104
+ test('Kimi assets are allowlisted and adapted to native tools and paths', () => {
105
+ const home = mkTarget();
106
+ const { pluginRoot } = layout(home);
107
+ try {
108
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
109
+
110
+ const skills = fs.readdirSync(path.join(pluginRoot, 'skills'), { withFileTypes: true })
111
+ .filter((entry) => entry.isDirectory()
112
+ && fs.existsSync(path.join(pluginRoot, 'skills', entry.name, 'SKILL.md')))
113
+ .map((entry) => entry.name)
114
+ .sort();
115
+ assert.deepEqual(skills, [...skillsForRuntime('kimi'), 'using-ultra-builder-pro'].sort());
116
+
117
+ const learn = fs.readFileSync(path.join(pluginRoot, 'skills', 'learn', 'SKILL.md'), 'utf8');
118
+ const review = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
119
+ const init = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-init', 'SKILL.md'), 'utf8');
120
+ const bootstrap = fs.readFileSync(
121
+ path.join(pluginRoot, 'skills', 'using-ultra-builder-pro', 'SKILL.md'),
122
+ 'utf8',
123
+ );
124
+ assert.match(learn, /~\/.kimi-code\/skills\/learned-<pattern-slug>-unverified\/SKILL\.md/);
125
+ assert.match(review, /Kimi `AgentSwarm`/);
126
+ assert.match(review, /Five engineering specialists/);
127
+ assert.match(review, /axes\.spec_fidelity/);
128
+ assert.doesNotMatch(review, /6 specialized|ALL 6/);
129
+ assert.match(review, /\$KIMI_PLUGIN_ROOT\/agents\/review-code\.md/);
130
+ assert.match(review, /`prompt_template` containing\s+`\{\{item\}\}`/);
131
+ assert.match(review, /selected reviewer names as the `items` array/);
132
+ assert.match(review, /Launch the coordinator with one foreground Kimi `Agent` call/);
133
+ assert.doesNotMatch(review, /background mode|Set Kimi `AgentSwarm` parallel execution|Launch review-coordinator with Kimi `AgentSwarm`/);
134
+ assert.match(init, /\| Kimi \| `\/ultra-builder-pro:ultra-init/);
135
+ assert.doesNotMatch(init, /codex exec "run \/ultra-init/);
136
+ assert.match(bootstrap, /\.ultra\/state\.db/);
137
+ assert.match(bootstrap, /external providers/);
138
+
139
+ const allPromptAssets = [];
140
+ for (const root of ['commands', 'skills', 'agents']) {
141
+ const pending = [path.join(pluginRoot, root)];
142
+ while (pending.length) {
143
+ const current = pending.pop();
144
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
145
+ const file = path.join(current, entry.name);
146
+ if (entry.isDirectory()) pending.push(file);
147
+ else if (entry.isFile() && /\.(?:md|json)$/.test(entry.name)) allPromptAssets.push(file);
148
+ }
149
+ }
150
+ }
151
+ const foreign = /~\/.claude|~\/.codex|~\/.config\/opencode|CLAUDE\.md|TaskCreate|TaskUpdate|TaskList|AskUserQuestion|run_in_background:\s*true|\$CLAUDE_PLUGIN_ROOT|Kimi `Shell`/;
152
+ for (const file of allPromptAssets) {
153
+ const text = fs.readFileSync(file, 'utf8');
154
+ assert.doesNotMatch(text, foreign, file);
155
+ assert.doesNotMatch(text, /(^|[\s`("'])\/ultra-(?!builder-pro:)[a-z]/m, file);
156
+ }
157
+ } finally {
158
+ fs.rmSync(home, { recursive: true, force: true });
159
+ }
160
+ });
161
+
162
+ test('reinstall preserves unrelated records and existing Kimi capability choices', () => {
163
+ const home = mkTarget();
164
+ const { registryFile } = layout(home);
165
+ try {
166
+ fs.mkdirSync(path.dirname(registryFile), { recursive: true });
167
+ fs.writeFileSync(registryFile, JSON.stringify({
168
+ version: 1,
169
+ plugins: [{
170
+ id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
171
+ installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
172
+ }],
173
+ }, null, 2));
174
+
175
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
176
+ const first = readJson(registryFile);
177
+ const ultra = first.plugins.find((entry) => entry.id === PLUGIN_ID);
178
+ ultra.enabled = false;
179
+ ultra.capabilities = { mcpServers: { [PLUGIN_ID]: { enabled: false } } };
180
+ fs.writeFileSync(registryFile, JSON.stringify(first, null, 2));
181
+
182
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
183
+ const second = readJson(registryFile);
184
+ assert.deepEqual(second.plugins.map((entry) => entry.id).sort(), ['mine', PLUGIN_ID]);
185
+ const refreshed = second.plugins.find((entry) => entry.id === PLUGIN_ID);
186
+ assert.equal(refreshed.installedAt, ultra.installedAt);
187
+ assert.equal(refreshed.enabled, false);
188
+ assert.deepEqual(refreshed.capabilities, ultra.capabilities);
189
+ } finally {
190
+ fs.rmSync(home, { recursive: true, force: true });
191
+ }
192
+ });
193
+
194
+ test('uninstall removes only the owned Kimi plugin record and managed root', () => {
195
+ const home = mkTarget();
196
+ const { pluginRoot, registryFile } = layout(home);
197
+ try {
198
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
199
+ const registry = readJson(registryFile);
200
+ registry.plugins.unshift({
201
+ id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
202
+ installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
203
+ });
204
+ fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
205
+
206
+ kimi.uninstall({ configDir: home });
207
+ assert.equal(fs.existsSync(pluginRoot), false);
208
+ assert.deepEqual(readJson(registryFile).plugins.map((entry) => entry.id), ['mine']);
209
+ } finally {
210
+ fs.rmSync(home, { recursive: true, force: true });
211
+ }
212
+ });
213
+
214
+ test('install and uninstall refuse to replace or remove an unmanaged Kimi root', () => {
215
+ const home = mkTarget();
216
+ const { pluginRoot, registryFile } = layout(home);
217
+ try {
218
+ fs.mkdirSync(pluginRoot, { recursive: true });
219
+ fs.writeFileSync(path.join(pluginRoot, 'user.txt'), 'keep');
220
+ assert.throws(
221
+ () => kimi.install({ configDir: home, repoRoot: REPO_ROOT }),
222
+ /unmanaged Kimi plugin/,
223
+ );
224
+
225
+ fs.mkdirSync(path.dirname(registryFile), { recursive: true });
226
+ fs.writeFileSync(registryFile, JSON.stringify({
227
+ version: 1,
228
+ plugins: [{
229
+ id: PLUGIN_ID, root: pluginRoot, source: 'local-path', enabled: true,
230
+ installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/foreign',
231
+ }],
232
+ }));
233
+ assert.throws(() => kimi.uninstall({ configDir: home }), /unmanaged Kimi plugin/);
234
+ assert.equal(fs.readFileSync(path.join(pluginRoot, 'user.txt'), 'utf8'), 'keep');
235
+ } finally {
236
+ fs.rmSync(home, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ test('doctor validates registration and provenance without mutating the Kimi home', () => {
241
+ const home = mkTarget();
242
+ const { registryFile } = layout(home);
243
+ try {
244
+ kimi.install({ configDir: home, repoRoot: REPO_ROOT });
245
+ const before = fs.readFileSync(registryFile);
246
+ const healthy = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
247
+ assert.equal(healthy.status, 'healthy', JSON.stringify(healthy, null, 2));
248
+ assert.deepEqual(fs.readFileSync(registryFile), before);
249
+
250
+ const registry = readJson(registryFile);
251
+ registry.plugins[0].enabled = false;
252
+ fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
253
+ const degraded = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
254
+ assert.equal(degraded.status, 'degraded');
255
+ assert.ok(degraded.issues.some((issue) => issue.code === 'PLUGIN_REGISTRATION_INVALID'));
256
+ } finally {
257
+ fs.rmSync(home, { recursive: true, force: true });
258
+ }
259
+ });
@@ -5,6 +5,7 @@ const assert = require('node:assert/strict');
5
5
  const fs = require('node:fs');
6
6
  const os = require('node:os');
7
7
  const path = require('node:path');
8
+ const { execFileSync } = require('node:child_process');
8
9
  const { pathToFileURL } = require('node:url');
9
10
 
10
11
  const opencode = require('../opencode.js');
@@ -172,7 +173,8 @@ test('install writes a schema-safe opencode.json and keeps ownership outside hos
172
173
  assert.match(plugin, /throw new Error/);
173
174
  assert.match(plugin, /session\.compacted/);
174
175
  assert.doesNotMatch(plugin, /memory\.(?:retain|recall|reflect)|journal|observation|prompt[_ -]?capture/);
175
- assert.match(plugin, /metadata references only/);
176
+ assert.match(plugin, /Ultra context spine/);
177
+ assert.doesNotMatch(plugin, /providerMetadata|External providers/);
176
178
  } finally {
177
179
  fs.rmSync(target, { recursive: true, force: true });
178
180
  }
@@ -189,18 +191,59 @@ test('OpenCode context covers active changes and projection writes are blocked f
189
191
 
190
192
  fs.mkdirSync(path.join(project, '.ultra'), { recursive: true });
191
193
  fs.writeFileSync(path.join(project, '.ultra', 'state.db'), 'owned-by-ultra');
194
+ execFileSync('git', ['init'], { cwd: project, stdio: 'ignore' });
195
+ fs.writeFileSync(path.join(project, 'app.txt'), 'initial\n');
196
+ execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
197
+ execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'initial'], {
198
+ cwd: project, stdio: 'ignore',
199
+ });
200
+ const compiledHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: project, encoding: 'utf8' }).trim();
192
201
  const changeRoot = path.join(project, '.ultra', 'changes', 'active', 'daily-fix');
193
202
  fs.mkdirSync(changeRoot, { recursive: true });
194
203
  fs.writeFileSync(path.join(changeRoot, 'context-manifest.json'), JSON.stringify({
204
+ schema_version: '2.0',
195
205
  change: { id: 'daily-fix', title: 'Daily fix', kind: 'quick', status: 'active', intent: 'Fix drift' },
206
+ role: 'implement',
207
+ gate: 'implementation',
208
+ next_action: 'Run the exact regression test.',
209
+ readiness: { status: 'ready', blockers: [] },
210
+ git: { head: compiledHead },
211
+ selected_task: { id: 'daily-task', status: 'in_progress' },
212
+ context: { token_estimate: 1200, budget: { max_tokens: 8000, max_files: 8 } },
196
213
  providers: { memory: { provider: 'cloud-mem', status: 'available' } },
197
214
  }));
198
215
 
199
216
  const output = { system: [] };
200
217
  await plugin['experimental.chat.system.transform']({}, output);
201
- assert.match(output.system.join('\n'), /Ultra continuous change/);
218
+ assert.match(output.system.join('\n'), /Ultra context spine/);
202
219
  assert.match(output.system.join('\n'), /daily-fix/);
203
- assert.match(output.system.join('\n'), /metadata references only/);
220
+ assert.match(output.system.join('\n'), /daily-task/);
221
+ assert.match(output.system.join('\n'), /Role: implement/);
222
+ assert.match(output.system.join('\n'), /Gate: implementation/);
223
+ assert.match(output.system.join('\n'), /Readiness: ready/);
224
+ assert.match(output.system.join('\n'), /Run the exact regression test/);
225
+ assert.doesNotMatch(output.system.join('\n'), /Fix drift|cloud-mem/);
226
+
227
+ const manifestFile = path.join(changeRoot, 'context-manifest.json');
228
+ const legacyManifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
229
+ legacyManifest.schema_version = '1.0';
230
+ fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
231
+ const legacyOutput = { system: [] };
232
+ await plugin['experimental.chat.system.transform']({}, legacyOutput);
233
+ assert.match(legacyOutput.system.join('\n'), /CONTEXT_SNAPSHOT_UPGRADE_REQUIRED/);
234
+ legacyManifest.schema_version = '2.0';
235
+ fs.writeFileSync(manifestFile, JSON.stringify(legacyManifest));
236
+
237
+ fs.writeFileSync(path.join(project, 'app.txt'), 'changed\n');
238
+ execFileSync('git', ['add', 'app.txt'], { cwd: project, stdio: 'ignore' });
239
+ execFileSync('git', ['-c', 'user.name=Ultra Tests', '-c', 'user.email=ultra@example.invalid', 'commit', '-m', 'change'], {
240
+ cwd: project, stdio: 'ignore',
241
+ });
242
+ const staleOutput = { system: [] };
243
+ await plugin['experimental.chat.system.transform']({}, staleOutput);
244
+ assert.match(staleOutput.system.join('\n'), /Readiness: blocked/);
245
+ assert.match(staleOutput.system.join('\n'), /CONTEXT_HEAD_STALE/);
246
+ assert.match(staleOutput.system.join('\n'), /recompile change\.context/);
204
247
 
205
248
  await assert.rejects(
206
249
  plugin['tool.execute.before'](
package/bin/handbook.js CHANGED
@@ -14,8 +14,8 @@ const {
14
14
  function usage() {
15
15
  return [
16
16
  'Usage:',
17
- ' ubp-handbook preview --runtime <claude|codex|opencode> [--file <path>]',
18
- ' ubp-handbook apply --runtime <claude|codex|opencode> [--file <path>]',
17
+ ' ubp-handbook preview --runtime <claude|codex|opencode|kimi> [--file <path>]',
18
+ ' ubp-handbook apply --runtime <claude|codex|opencode|kimi> [--file <path>]',
19
19
  '',
20
20
  'The apply command creates a timestamped backup before changing an existing file.',
21
21
  ].join('\n');