ultra-builder-pro-cli 0.8.1 → 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.
- package/CHANGELOG.md +32 -1
- package/README.md +36 -22
- package/adapters/_shared/codex-assets.cjs +28 -0
- package/adapters/_shared/handbook.cjs +8 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/runtime-assets.cjs +2 -1
- package/adapters/_shared/tests/handbook.test.cjs +6 -1
- package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
- package/adapters/_shared/tests/runtime-assets.test.cjs +4 -1
- package/adapters/kimi.js +642 -0
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +255 -0
- package/bin/handbook.js +2 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +1 -1
- package/docs/AGENT-CONTEXT.md +5 -3
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/PLAN.zh-CN.md +4 -4
- package/docs/ROADMAP.md +4 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -62
- package/docs/USER-HANDBOOK-CONTRACT.md +4 -1
- package/hooks/adapters/kimi.py +162 -0
- package/mcp-server/lib/state-db.cjs +85 -2
- package/mcp-server/lib/state-db.test.cjs +56 -0
- package/mcp-server/tests/init-project.test.cjs +2 -2
- package/mcp-server/tests/state-db.test.cjs +3 -3
- package/package.json +4 -2
- package/skills/ultra-review/SKILL.md +3 -3
- package/spec/mcp-tools.yaml +3 -3
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +4 -2
|
@@ -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,255 @@
|
|
|
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) assert.match(hookText, new RegExp(hook.replace('.', '\\.')));
|
|
89
|
+
assert.doesNotMatch(hookText, /memory|recall|journal|prompt[_ -]?capture|block_dangerous|post_edit_guard/i);
|
|
90
|
+
|
|
91
|
+
const commands = fs.readdirSync(path.join(pluginRoot, 'commands')).sort();
|
|
92
|
+
assert.deepEqual(commands, CORE_PUBLIC_SKILLS.map((name) => `${name}.md`).sort());
|
|
93
|
+
const commandText = fs.readFileSync(path.join(pluginRoot, 'commands', 'ultra-init.md'), 'utf8');
|
|
94
|
+
assert.match(commandText, /Use the registered `ultra-init` skill/);
|
|
95
|
+
assert.match(commandText, /\$ARGUMENTS/);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('Kimi assets are allowlisted and adapted to native tools and paths', () => {
|
|
102
|
+
const home = mkTarget();
|
|
103
|
+
const { pluginRoot } = layout(home);
|
|
104
|
+
try {
|
|
105
|
+
kimi.install({ configDir: home, repoRoot: REPO_ROOT });
|
|
106
|
+
|
|
107
|
+
const skills = fs.readdirSync(path.join(pluginRoot, 'skills'), { withFileTypes: true })
|
|
108
|
+
.filter((entry) => entry.isDirectory()
|
|
109
|
+
&& fs.existsSync(path.join(pluginRoot, 'skills', entry.name, 'SKILL.md')))
|
|
110
|
+
.map((entry) => entry.name)
|
|
111
|
+
.sort();
|
|
112
|
+
assert.deepEqual(skills, [...skillsForRuntime('kimi'), 'using-ultra-builder-pro'].sort());
|
|
113
|
+
|
|
114
|
+
const learn = fs.readFileSync(path.join(pluginRoot, 'skills', 'learn', 'SKILL.md'), 'utf8');
|
|
115
|
+
const review = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-review', 'SKILL.md'), 'utf8');
|
|
116
|
+
const init = fs.readFileSync(path.join(pluginRoot, 'skills', 'ultra-init', 'SKILL.md'), 'utf8');
|
|
117
|
+
const bootstrap = fs.readFileSync(
|
|
118
|
+
path.join(pluginRoot, 'skills', 'using-ultra-builder-pro', 'SKILL.md'),
|
|
119
|
+
'utf8',
|
|
120
|
+
);
|
|
121
|
+
assert.match(learn, /~\/.kimi-code\/skills\/learned-<pattern-slug>-unverified\/SKILL\.md/);
|
|
122
|
+
assert.match(review, /Kimi `AgentSwarm`/);
|
|
123
|
+
assert.match(review, /5 specialized reviewers/);
|
|
124
|
+
assert.doesNotMatch(review, /6 specialized|ALL 6/);
|
|
125
|
+
assert.match(review, /\$KIMI_PLUGIN_ROOT\/agents\/review-code\.md/);
|
|
126
|
+
assert.match(review, /`prompt_template` containing\s+`\{\{item\}\}`/);
|
|
127
|
+
assert.match(review, /selected reviewer names as the `items` array/);
|
|
128
|
+
assert.match(review, /Launch the coordinator with one foreground Kimi `Agent` call/);
|
|
129
|
+
assert.doesNotMatch(review, /background mode|Set Kimi `AgentSwarm` parallel execution|Launch review-coordinator with Kimi `AgentSwarm`/);
|
|
130
|
+
assert.match(init, /\| Kimi \| `\/ultra-builder-pro:ultra-init/);
|
|
131
|
+
assert.doesNotMatch(init, /codex exec "run \/ultra-init/);
|
|
132
|
+
assert.match(bootstrap, /\.ultra\/state\.db/);
|
|
133
|
+
assert.match(bootstrap, /external providers/);
|
|
134
|
+
|
|
135
|
+
const allPromptAssets = [];
|
|
136
|
+
for (const root of ['commands', 'skills', 'agents']) {
|
|
137
|
+
const pending = [path.join(pluginRoot, root)];
|
|
138
|
+
while (pending.length) {
|
|
139
|
+
const current = pending.pop();
|
|
140
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
141
|
+
const file = path.join(current, entry.name);
|
|
142
|
+
if (entry.isDirectory()) pending.push(file);
|
|
143
|
+
else if (entry.isFile() && /\.(?:md|json)$/.test(entry.name)) allPromptAssets.push(file);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const foreign = /~\/.claude|~\/.codex|~\/.config\/opencode|CLAUDE\.md|TaskCreate|TaskUpdate|TaskList|AskUserQuestion|run_in_background:\s*true|\$CLAUDE_PLUGIN_ROOT|Kimi `Shell`/;
|
|
148
|
+
for (const file of allPromptAssets) {
|
|
149
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
150
|
+
assert.doesNotMatch(text, foreign, file);
|
|
151
|
+
assert.doesNotMatch(text, /(^|[\s`("'])\/ultra-(?!builder-pro:)[a-z]/m, file);
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('reinstall preserves unrelated records and existing Kimi capability choices', () => {
|
|
159
|
+
const home = mkTarget();
|
|
160
|
+
const { registryFile } = layout(home);
|
|
161
|
+
try {
|
|
162
|
+
fs.mkdirSync(path.dirname(registryFile), { recursive: true });
|
|
163
|
+
fs.writeFileSync(registryFile, JSON.stringify({
|
|
164
|
+
version: 1,
|
|
165
|
+
plugins: [{
|
|
166
|
+
id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
|
|
167
|
+
installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
|
|
168
|
+
}],
|
|
169
|
+
}, null, 2));
|
|
170
|
+
|
|
171
|
+
kimi.install({ configDir: home, repoRoot: REPO_ROOT });
|
|
172
|
+
const first = readJson(registryFile);
|
|
173
|
+
const ultra = first.plugins.find((entry) => entry.id === PLUGIN_ID);
|
|
174
|
+
ultra.enabled = false;
|
|
175
|
+
ultra.capabilities = { mcpServers: { [PLUGIN_ID]: { enabled: false } } };
|
|
176
|
+
fs.writeFileSync(registryFile, JSON.stringify(first, null, 2));
|
|
177
|
+
|
|
178
|
+
kimi.install({ configDir: home, repoRoot: REPO_ROOT });
|
|
179
|
+
const second = readJson(registryFile);
|
|
180
|
+
assert.deepEqual(second.plugins.map((entry) => entry.id).sort(), ['mine', PLUGIN_ID]);
|
|
181
|
+
const refreshed = second.plugins.find((entry) => entry.id === PLUGIN_ID);
|
|
182
|
+
assert.equal(refreshed.installedAt, ultra.installedAt);
|
|
183
|
+
assert.equal(refreshed.enabled, false);
|
|
184
|
+
assert.deepEqual(refreshed.capabilities, ultra.capabilities);
|
|
185
|
+
} finally {
|
|
186
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('uninstall removes only the owned Kimi plugin record and managed root', () => {
|
|
191
|
+
const home = mkTarget();
|
|
192
|
+
const { pluginRoot, registryFile } = layout(home);
|
|
193
|
+
try {
|
|
194
|
+
kimi.install({ configDir: home, repoRoot: REPO_ROOT });
|
|
195
|
+
const registry = readJson(registryFile);
|
|
196
|
+
registry.plugins.unshift({
|
|
197
|
+
id: 'mine', root: '/tmp/mine', source: 'local-path', enabled: true,
|
|
198
|
+
installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/mine',
|
|
199
|
+
});
|
|
200
|
+
fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
|
|
201
|
+
|
|
202
|
+
kimi.uninstall({ configDir: home });
|
|
203
|
+
assert.equal(fs.existsSync(pluginRoot), false);
|
|
204
|
+
assert.deepEqual(readJson(registryFile).plugins.map((entry) => entry.id), ['mine']);
|
|
205
|
+
} finally {
|
|
206
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('install and uninstall refuse to replace or remove an unmanaged Kimi root', () => {
|
|
211
|
+
const home = mkTarget();
|
|
212
|
+
const { pluginRoot, registryFile } = layout(home);
|
|
213
|
+
try {
|
|
214
|
+
fs.mkdirSync(pluginRoot, { recursive: true });
|
|
215
|
+
fs.writeFileSync(path.join(pluginRoot, 'user.txt'), 'keep');
|
|
216
|
+
assert.throws(
|
|
217
|
+
() => kimi.install({ configDir: home, repoRoot: REPO_ROOT }),
|
|
218
|
+
/unmanaged Kimi plugin/,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
fs.mkdirSync(path.dirname(registryFile), { recursive: true });
|
|
222
|
+
fs.writeFileSync(registryFile, JSON.stringify({
|
|
223
|
+
version: 1,
|
|
224
|
+
plugins: [{
|
|
225
|
+
id: PLUGIN_ID, root: pluginRoot, source: 'local-path', enabled: true,
|
|
226
|
+
installedAt: '2026-01-01T00:00:00.000Z', originalSource: '/tmp/foreign',
|
|
227
|
+
}],
|
|
228
|
+
}));
|
|
229
|
+
assert.throws(() => kimi.uninstall({ configDir: home }), /unmanaged Kimi plugin/);
|
|
230
|
+
assert.equal(fs.readFileSync(path.join(pluginRoot, 'user.txt'), 'utf8'), 'keep');
|
|
231
|
+
} finally {
|
|
232
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('doctor validates registration and provenance without mutating the Kimi home', () => {
|
|
237
|
+
const home = mkTarget();
|
|
238
|
+
const { registryFile } = layout(home);
|
|
239
|
+
try {
|
|
240
|
+
kimi.install({ configDir: home, repoRoot: REPO_ROOT });
|
|
241
|
+
const before = fs.readFileSync(registryFile);
|
|
242
|
+
const healthy = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
|
|
243
|
+
assert.equal(healthy.status, 'healthy', JSON.stringify(healthy, null, 2));
|
|
244
|
+
assert.deepEqual(fs.readFileSync(registryFile), before);
|
|
245
|
+
|
|
246
|
+
const registry = readJson(registryFile);
|
|
247
|
+
registry.plugins[0].enabled = false;
|
|
248
|
+
fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
|
|
249
|
+
const degraded = kimi.doctor({ configDir: home, repoRoot: REPO_ROOT });
|
|
250
|
+
assert.equal(degraded.status, 'degraded');
|
|
251
|
+
assert.ok(degraded.issues.some((issue) => issue.code === 'PLUGIN_REGISTRATION_INVALID'));
|
|
252
|
+
} finally {
|
|
253
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
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');
|
package/bin/install.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* ultra-builder-pro-cli — multi-runtime installer.
|
|
5
5
|
*
|
|
6
6
|
* Distributes Ultra Builder Pro assets — commands, agents, skills, hooks,
|
|
7
|
-
* MCP server — to Claude Code, OpenCode,
|
|
7
|
+
* MCP server — to Claude Code, OpenCode, Codex CLI, and Kimi Code via
|
|
8
8
|
* runtime-specific adapters under adapters/. Install is idempotent and
|
|
9
9
|
* uses atomic writes; uninstall reverses via sentinel/manifest blocks.
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* npx ultra-builder-pro-cli [options]
|
|
13
13
|
*
|
|
14
|
-
* --claude / --opencode / --codex
|
|
14
|
+
* --claude / --opencode / --codex / --kimi select runtime(s)
|
|
15
15
|
* --all install to all supported runtimes
|
|
16
16
|
* -g, --global install to runtime's global config dir
|
|
17
17
|
* -l, --local install into current working directory
|
|
@@ -58,7 +58,7 @@ function printBanner() {
|
|
|
58
58
|
paint('cyan', ' ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚═╝'),
|
|
59
59
|
'',
|
|
60
60
|
` ${paint('bold', 'Ultra Builder Pro CLI')} ${paint('dim', 'v' + pkg.version)}`,
|
|
61
|
-
` ${paint('dim', 'Multi-runtime installer for Claude Code, OpenCode, and
|
|
61
|
+
` ${paint('dim', 'Multi-runtime installer for Claude Code, OpenCode, Codex, and Kimi Code')}`,
|
|
62
62
|
'',
|
|
63
63
|
];
|
|
64
64
|
console.log(banner.join('\n'));
|
|
@@ -71,6 +71,7 @@ function printHelp() {
|
|
|
71
71
|
${paint('cyan', '--claude')} Claude Code
|
|
72
72
|
${paint('cyan', '--opencode')} OpenCode
|
|
73
73
|
${paint('cyan', '--codex')} Codex CLI (OpenAI)
|
|
74
|
+
${paint('cyan', '--kimi')} Kimi Code (Moonshot AI)
|
|
74
75
|
${paint('cyan', '--all')} all supported runtimes
|
|
75
76
|
|
|
76
77
|
${paint('yellow', 'Scope:')}
|
|
@@ -95,6 +96,9 @@ function printHelp() {
|
|
|
95
96
|
${paint('dim', '# Uninstall from OpenCode')}
|
|
96
97
|
npx ultra-builder-pro-cli --opencode --global --uninstall
|
|
97
98
|
|
|
99
|
+
${paint('dim', '# Install to Kimi Code globally')}
|
|
100
|
+
npx ultra-builder-pro-cli --kimi --global
|
|
101
|
+
|
|
98
102
|
${paint('dim', '# Verify all host installations without changing them')}
|
|
99
103
|
npx ultra-builder-pro-cli --all --global --doctor
|
|
100
104
|
`);
|
|
@@ -120,6 +124,7 @@ function parseArgs(argv) {
|
|
|
120
124
|
case '--claude': runtimes.add('claude'); break;
|
|
121
125
|
case '--opencode': runtimes.add('opencode'); break;
|
|
122
126
|
case '--codex': runtimes.add('codex'); break;
|
|
127
|
+
case '--kimi': runtimes.add('kimi'); break;
|
|
123
128
|
case '--all':
|
|
124
129
|
SUPPORTED_RUNTIMES.forEach(r => runtimes.add(r));
|
|
125
130
|
break;
|
|
@@ -199,7 +204,7 @@ async function main() {
|
|
|
199
204
|
if (flags.json && !flags.doctor) bail('--json is available only with --doctor');
|
|
200
205
|
|
|
201
206
|
if (runtimes.length === 0) {
|
|
202
|
-
bail('no runtime selected; use --claude / --opencode / --codex / --all');
|
|
207
|
+
bail('no runtime selected; use --claude / --opencode / --codex / --kimi / --all');
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
const scope = resolveScope(flags);
|
package/bin/orchestrator.js
CHANGED
|
@@ -47,7 +47,7 @@ function optInAllowed(settings) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function parseRuntimes() {
|
|
50
|
-
const raw = process.env.UBP_ORCH_RUNTIMES || 'claude,opencode,codex';
|
|
50
|
+
const raw = process.env.UBP_ORCH_RUNTIMES || 'claude,opencode,codex,kimi';
|
|
51
51
|
const runtimes = [...new Set(raw.split(',').map((s) => s.trim()).filter(Boolean))];
|
|
52
52
|
const invalid = runtimes.filter((runtime) => !isSupportedRuntime(runtime));
|
|
53
53
|
if (invalid.length > 0) {
|
package/docs/AGENT-CONTEXT.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Ultra Builder Pro — Agent Context
|
|
2
2
|
|
|
3
|
-
Current shared runtime contract for the native Claude Code, OpenCode,
|
|
4
|
-
plugins.
|
|
3
|
+
Current shared runtime contract for the native Claude Code, OpenCode, Codex,
|
|
4
|
+
and Kimi Code plugins.
|
|
5
5
|
|
|
6
6
|
## 1. Ownership boundary
|
|
7
7
|
|
|
@@ -12,7 +12,8 @@ Ultra Builder Pro owns only:
|
|
|
12
12
|
`ultra-think`, plus the daily `ultra-change` and diagnostic `ultra-doctor`;
|
|
13
13
|
- four internal agent-only rule skills: `code-review-expert`, `security-rules`,
|
|
14
14
|
`integration-rules`, and `testing-rules`;
|
|
15
|
-
- the host-specific collaboration companions
|
|
15
|
+
- the host-specific collaboration companions, `ultra-verify`, and the minimal
|
|
16
|
+
Kimi session bootstrap required to establish the shared runtime boundary;
|
|
16
17
|
- bounded review/debug agents, workflow-only hooks, MCP task state, and the
|
|
17
18
|
portable CLI/orchestrator.
|
|
18
19
|
|
|
@@ -68,6 +69,7 @@ fixtures. Do not add an ad-hoc server handler first.
|
|
|
68
69
|
| Claude Code | `.claude-plugin/plugin.json`, native commands/skills/agents, `.mcp.json` | `/ultra-*`, `/learn` | native `hooks/hooks.json` | `codex-collab`, `ultra-verify` |
|
|
69
70
|
| Codex | personal plugin with `.codex-plugin/plugin.json`, namespaced skills, TOML agents, `.mcp.json` | `$ultra-builder-pro:ultra-*`, `$ultra-builder-pro:learn` | native `hooks/hooks.json` through the Codex wire adapter | `cc-collab`, `ultra-verify` |
|
|
70
71
|
| OpenCode | config bundle plus native JavaScript plugin | `/ultra-*`, `/learn` | `event`, system transform, compaction, and tool lifecycle handlers | `cc-collab`, `codex-collab`, `ultra-verify` |
|
|
72
|
+
| Kimi Code 0.26+ | managed `kimi.plugin.json` plugin with commands, skills, hooks, and MCP | `/ultra-builder-pro:ultra-*`, `/ultra-builder-pro:learn` | native hooks through the Kimi wire adapter | `cc-collab`, `codex-collab`, `ultra-verify` |
|
|
71
73
|
|
|
72
74
|
The current host remains primary. Collaboration skills call another runtime
|
|
73
75
|
only when explicitly requested, use it as a read-only advisor, and return the
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
`ultra-builder-pro-cli` is a multi-runtime plugin suite that **distributes** the
|
|
12
12
|
Ultra Builder Pro engineering loop (skills + commands + workflow hooks) as
|
|
13
|
-
native Claude Code, OpenCode, and
|
|
13
|
+
native Claude Code, OpenCode, Codex, and Kimi Code plugins, and
|
|
14
14
|
**runs** that loop with isolated sessions sharing one authoritative state
|
|
15
15
|
store (`.ultra/state.db`).
|
|
16
16
|
|
|
@@ -23,7 +23,7 @@ the supported hosts share.
|
|
|
23
23
|
┌──────────────────────────┐
|
|
24
24
|
│ runtime CLI │
|
|
25
25
|
│ (claude / opencode / │
|
|
26
|
-
│ codex)
|
|
26
|
+
│ codex / kimi) │
|
|
27
27
|
└────────────┬─────────────┘
|
|
28
28
|
│
|
|
29
29
|
┌───────────────────┼────────────────────┐
|
|
@@ -156,7 +156,7 @@ backup-first mechanical recovery when explicitly requested.
|
|
|
156
156
|
|
|
157
157
|
## 5. Sessions — the execution unit
|
|
158
158
|
|
|
159
|
-
A **session** is the standard unit of execution across all
|
|
159
|
+
A **session** is the standard unit of execution across all four runtimes
|
|
160
160
|
(D20). One session =
|
|
161
161
|
|
|
162
162
|
- a fresh OS process for the runtime,
|
|
@@ -189,7 +189,7 @@ durable authority alongside `.ultra/state.db`.
|
|
|
189
189
|
|
|
190
190
|
## 7. Installation provenance — read-only cross-host diagnosis
|
|
191
191
|
|
|
192
|
-
Every Claude Code, OpenCode, and
|
|
192
|
+
Every Claude Code, OpenCode, Codex, and Kimi Code adapter writes a normalized
|
|
193
193
|
`provenance.json` for the assets it owns. The manifest records adapter/package
|
|
194
194
|
identity, source metadata, per-file SHA-256 hashes, an aggregate digest, and the
|
|
195
195
|
host-specific plugin, MCP, hook, launcher, and runtime contracts expected at
|
package/docs/PLAN.zh-CN.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
> 插件只保留 12 个 Ultra 公共 workflow(新增 daily `ultra-change` 与诊断
|
|
7
7
|
> `ultra-doctor`)、4 个 agent-only 规则 skill、宿主专属 collab companion、
|
|
8
8
|
> workflow/change hook 与 `.ultra/state.db` 权威。Memory 和代码图谱内容全部由外部
|
|
9
|
-
> provider 所有,Ultra 只保存 provider 元数据引用。Claude Code、Codex、OpenCode
|
|
10
|
-
> 采用各自 native plugin 呈现,其他旧 runtime adapter 已退役。下文所有与此冲突的
|
|
9
|
+
> provider 所有,Ultra 只保存 provider 元数据引用。Claude Code、Codex、OpenCode、
|
|
10
|
+
> Kimi Code 采用各自 native plugin 呈现,其他旧 runtime adapter 已退役。下文所有与此冲突的
|
|
11
11
|
> memory/hindsight 章节仅作为历史实施记录,不再是当前需求或运行时契约。
|
|
12
12
|
|
|
13
|
-
**当前状态**:v0.
|
|
14
|
-
schema 9.
|
|
13
|
+
**当前状态**:v0.9.0;四宿主 native plugin、29 个 live MCP tool、
|
|
14
|
+
schema 9.1(15 表)、持续 change/context/convergence/doctor、incident debug lane、
|
|
15
15
|
checkpoint 恢复消费与跨宿主安装 provenance/doctor 已落地;npm 发布由
|
|
16
16
|
`v*.*.*` tag 触发。以下 Phase 0-9 正文保留为 2026-04 的实施历史,不再作为
|
|
17
17
|
当前 runtime 或发布状态的事实来源。
|
package/docs/ROADMAP.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
> when historical plan text differs.
|
|
7
7
|
|
|
8
8
|
**Goal**: distribute the Ultra Builder Pro engineering loop as native Claude
|
|
9
|
-
Code, OpenCode, and
|
|
10
|
-
authoritative workflow store.
|
|
9
|
+
Code, OpenCode, Codex, and Kimi Code plugins, and run it with isolated sessions
|
|
10
|
+
sharing one authoritative workflow store.
|
|
11
11
|
|
|
12
|
-
**First-class runtimes**: Claude Code · OpenCode · Codex.
|
|
12
|
+
**First-class runtimes**: Claude Code · OpenCode · Codex · Kimi Code.
|
|
13
13
|
|
|
14
14
|
**Distribution channels (v1.0)**: npm · Homebrew · pip.
|
|
15
15
|
|
|
16
|
-
**Release line**: `0.
|
|
16
|
+
**Release line**: `0.9.x`; every release is gated by `npm run verify:release`.
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|