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.
- package/CHANGELOG.md +60 -1
- package/README.md +49 -23
- package/adapters/_shared/codex-assets.cjs +28 -0
- package/adapters/_shared/handbook.cjs +9 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/provenance.cjs +286 -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/provenance.test.cjs +133 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +4 -1
- package/adapters/claude.js +33 -0
- package/adapters/codex.js +87 -0
- package/adapters/kimi.js +642 -0
- package/adapters/opencode.js +66 -0
- package/adapters/tests/codex.test.cjs +37 -0
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +255 -0
- package/agents/debugger.md +12 -0
- package/bin/handbook.js +2 -2
- package/bin/install.js +72 -12
- package/bin/orchestrator.js +1 -1
- package/commands/ultra-change.md +3 -2
- package/docs/AGENT-CONTEXT.md +5 -3
- package/docs/ARCHITECTURE.md +40 -6
- package/docs/PLAN.zh-CN.md +8 -6
- package/docs/ROADMAP.md +4 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +109 -58
- package/docs/USER-HANDBOOK-CONTRACT.md +4 -1
- package/hooks/README.md +6 -2
- package/hooks/adapters/kimi.py +162 -0
- package/hooks/workflow_checkpoint.py +15 -1
- package/hooks/workflow_resume.py +96 -7
- package/mcp-server/lib/change-workflow.cjs +69 -0
- package/mcp-server/lib/change-workflow.test.cjs +87 -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-change/SKILL.md +12 -3
- package/skills/ultra-deliver/SKILL.md +3 -1
- package/skills/ultra-doctor/SKILL.md +10 -3
- package/skills/ultra-review/SKILL.md +3 -3
- package/spec/mcp-tools.yaml +5 -5
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +4 -2
package/adapters/opencode.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
writeAtomic,
|
|
16
16
|
} = require('./_shared/file-ops.cjs');
|
|
17
17
|
const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
|
|
18
|
+
const provenance = require('./_shared/provenance.cjs');
|
|
18
19
|
const {
|
|
19
20
|
CORE_PUBLIC_SKILLS,
|
|
20
21
|
skillsForRuntime,
|
|
@@ -34,6 +35,7 @@ const SOURCE_TAG = 'ubp';
|
|
|
34
35
|
const BUNDLE_DIR = '.ultra-builder-pro';
|
|
35
36
|
const MANAGED_TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
36
37
|
const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
38
|
+
const PROVENANCE_FILE = 'provenance.json';
|
|
37
39
|
const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
|
|
38
40
|
|
|
39
41
|
function resolveTarget(ctx = {}) {
|
|
@@ -515,6 +517,69 @@ function install(ctx = {}) {
|
|
|
515
517
|
delete next[LEGACY_SENTINEL_KEY];
|
|
516
518
|
writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
|
|
517
519
|
report.config.updated = true;
|
|
520
|
+
const source = provenance.packageSource(repoRoot);
|
|
521
|
+
const bundleAssets = provenance.assetRefsForTree('config', bundleRoot, {
|
|
522
|
+
exclude: [PROVENANCE_FILE],
|
|
523
|
+
}).map((asset) => ({ ...asset, path: path.join(BUNDLE_DIR, asset.path) }));
|
|
524
|
+
const skillMarkers = skillsForRuntime('opencode').map((name) => ({
|
|
525
|
+
root: 'config', path: path.join('skills', name, '.ubp-managed'),
|
|
526
|
+
}));
|
|
527
|
+
const provenanceFile = path.join(bundleRoot, PROVENANCE_FILE);
|
|
528
|
+
report.provenance = provenance.writeProvenance({
|
|
529
|
+
file: provenanceFile,
|
|
530
|
+
adapter: 'opencode',
|
|
531
|
+
...source,
|
|
532
|
+
roots: { config: target },
|
|
533
|
+
assets: [
|
|
534
|
+
...report.copied.commands.map((file) => ({ root: 'config', path: path.join('commands', file) })),
|
|
535
|
+
...report.copied.skills.map((file) => ({ root: 'config', path: path.join('skills', file) })),
|
|
536
|
+
...skillMarkers,
|
|
537
|
+
...report.copied.agents.map((file) => ({ root: 'config', path: path.join('agents', file) })),
|
|
538
|
+
{ root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
539
|
+
...bundleAssets,
|
|
540
|
+
],
|
|
541
|
+
contracts: {
|
|
542
|
+
host_plugin: { root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
543
|
+
mcp_config: { root: 'config', path: 'opencode.json' },
|
|
544
|
+
mcp_launcher: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'launch.cjs') },
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
report.provenance.file = provenanceFile;
|
|
548
|
+
return report;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function doctor(ctx = {}) {
|
|
552
|
+
const target = resolveTarget(ctx);
|
|
553
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
554
|
+
const source = provenance.packageSource(repoRoot);
|
|
555
|
+
const report = provenance.inspectProvenance({
|
|
556
|
+
file: path.join(target, BUNDLE_DIR, PROVENANCE_FILE),
|
|
557
|
+
expectedAdapter: 'opencode',
|
|
558
|
+
expectedPackageVersion: source.packageInfo.version,
|
|
559
|
+
});
|
|
560
|
+
const configFile = path.join(target, 'opencode.json');
|
|
561
|
+
const expectedLauncher = path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs');
|
|
562
|
+
let registrationOk = false;
|
|
563
|
+
try {
|
|
564
|
+
const config = readJsonSafe(configFile);
|
|
565
|
+
const entry = config.mcp && config.mcp[MCP_SERVER_NAME];
|
|
566
|
+
registrationOk = entry?.type === 'local'
|
|
567
|
+
&& entry.enabled === true
|
|
568
|
+
&& Array.isArray(entry.command)
|
|
569
|
+
&& entry.command[0] === process.execPath
|
|
570
|
+
&& entry.command[1] === expectedLauncher;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
report.issues.push({ code: 'MCP_REGISTRATION_INVALID', message: error.message });
|
|
573
|
+
}
|
|
574
|
+
if (!registrationOk && !report.issues.some((entry) => entry.code === 'MCP_REGISTRATION_INVALID')) {
|
|
575
|
+
report.issues.push({
|
|
576
|
+
code: 'MCP_REGISTRATION_INVALID',
|
|
577
|
+
path: configFile,
|
|
578
|
+
expected_launcher: expectedLauncher,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
|
|
582
|
+
if (report.status !== 'missing') report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
518
583
|
return report;
|
|
519
584
|
}
|
|
520
585
|
|
|
@@ -590,4 +655,5 @@ module.exports = {
|
|
|
590
655
|
resolveTarget,
|
|
591
656
|
install,
|
|
592
657
|
uninstall,
|
|
658
|
+
doctor,
|
|
593
659
|
};
|
|
@@ -362,3 +362,40 @@ test('uninstall removes only UBP-managed plugin and agents', () => {
|
|
|
362
362
|
cleanup(layout);
|
|
363
363
|
}
|
|
364
364
|
});
|
|
365
|
+
|
|
366
|
+
test('doctor checks the current Codex cache hook target for a CLI-managed global install', () => {
|
|
367
|
+
const layout = mkLayout();
|
|
368
|
+
try {
|
|
369
|
+
install(layout);
|
|
370
|
+
const runtimeManifest = JSON.parse(fs.readFileSync(
|
|
371
|
+
path.join(layout.configDir, 'ultra-builder-pro', 'install-manifest.json'),
|
|
372
|
+
'utf8',
|
|
373
|
+
));
|
|
374
|
+
const version = runtimeManifest.plugin.version;
|
|
375
|
+
const cacheAdapter = path.join(
|
|
376
|
+
codex._internal.pluginCacheRoot(layout.configDir, 'personal'),
|
|
377
|
+
version,
|
|
378
|
+
'hooks',
|
|
379
|
+
'adapters',
|
|
380
|
+
'codex.py',
|
|
381
|
+
);
|
|
382
|
+
const doctorCtx = {
|
|
383
|
+
homeDir: layout.homeDir,
|
|
384
|
+
scope: 'global',
|
|
385
|
+
repoRoot: REPO_ROOT,
|
|
386
|
+
runPluginCli: true,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const degraded = codex.doctor(doctorCtx);
|
|
390
|
+
assert.equal(degraded.status, 'degraded');
|
|
391
|
+
assert.ok(degraded.issues.some((entry) => (
|
|
392
|
+
entry.code === 'HOOK_TARGET_MISSING' && entry.version === version
|
|
393
|
+
)));
|
|
394
|
+
|
|
395
|
+
fs.mkdirSync(path.dirname(cacheAdapter), { recursive: true });
|
|
396
|
+
fs.copyFileSync(path.join(layout.pluginRoot, 'hooks', 'adapters', 'codex.py'), cacheAdapter);
|
|
397
|
+
assert.equal(codex.doctor(doctorCtx).status, 'healthy');
|
|
398
|
+
} finally {
|
|
399
|
+
cleanup(layout);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
@@ -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/agents/debugger.md
CHANGED
|
@@ -115,6 +115,18 @@ If **3 consecutive fix attempts fail**, and each reveals new problems in differe
|
|
|
115
115
|
{test output proving fix resolves the issue, no regressions}
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
When the parent assigns an Ultra `incident` change, also map the verified report
|
|
119
|
+
into the exact durable `diagnosis.md` contract:
|
|
120
|
+
|
|
121
|
+
- `Reproduction`: the smallest repeatable symptom and exact trigger;
|
|
122
|
+
- `Hypotheses`: falsifiable hypotheses and the evidence that accepted/rejected each;
|
|
123
|
+
- `Root cause`: the earliest incorrect state and why it occurred;
|
|
124
|
+
- `Regression test`: the failing-first test and its final passing command;
|
|
125
|
+
- `Recovery`: rollback, retry, repair, or operator action when applicable.
|
|
126
|
+
|
|
127
|
+
Return this content to the primary agent. Do not write `.ultra/state.db` or claim
|
|
128
|
+
incident convergence; the primary agent owns the change artifact and verification.
|
|
129
|
+
|
|
118
130
|
## Rules
|
|
119
131
|
|
|
120
132
|
- Never guess. Every claim must have evidence from code or output.
|
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');
|