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.
- package/CHANGELOG.md +79 -1
- package/README.md +45 -26
- package/adapters/_shared/codex-assets.cjs +38 -0
- package/adapters/_shared/handbook.cjs +10 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/runtime-assets.cjs +3 -1
- package/adapters/_shared/tests/handbook.test.cjs +9 -1
- package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
- package/adapters/_shared/tests/runtime-assets.test.cjs +5 -1
- package/adapters/kimi.js +642 -0
- package/adapters/opencode.js +46 -17
- package/adapters/tests/claude.test.cjs +4 -1
- package/adapters/tests/codex.test.cjs +4 -3
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +259 -0
- package/adapters/tests/opencode.test.cjs +46 -3
- package/bin/handbook.js +2 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +1 -1
- package/docs/AGENT-CONTEXT.md +39 -15
- package/docs/ARCHITECTURE.md +35 -9
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +8 -7
- package/docs/RUNTIME-COMPAT-MATRIX.md +110 -64
- package/docs/USER-HANDBOOK-CONTRACT.md +13 -3
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -0
- package/hooks/adapters/kimi.py +162 -0
- package/hooks/context_spine.py +218 -0
- package/hooks/health_check.py +1 -1
- package/hooks/workflow_context.py +16 -123
- package/hooks/workflow_resume.py +21 -0
- package/mcp-server/lib/change-workflow.cjs +89 -11
- package/mcp-server/lib/change-workflow.test.cjs +202 -7
- package/mcp-server/lib/context-spine.cjs +331 -0
- package/mcp-server/lib/context-spine.test.cjs +73 -0
- package/mcp-server/lib/spec-learning.cjs +191 -0
- package/mcp-server/lib/spec-learning.test.cjs +62 -0
- package/mcp-server/lib/state-db.cjs +149 -4
- package/mcp-server/lib/state-db.test.cjs +61 -0
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/init-project.test.cjs +2 -2
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +4 -2
- package/skills/learn/SKILL.md +1 -0
- package/skills/ultra-change/SKILL.md +91 -118
- package/skills/ultra-deliver/SKILL.md +93 -221
- package/skills/ultra-dev/SKILL.md +81 -310
- package/skills/ultra-doctor/SKILL.md +1 -0
- package/skills/ultra-init/SKILL.md +1 -0
- package/skills/ultra-plan/SKILL.md +72 -292
- package/skills/ultra-review/SKILL.md +79 -407
- package/skills/ultra-status/SKILL.md +55 -168
- package/skills/ultra-test/SKILL.md +64 -183
- package/skills/ultra-think/SKILL.md +1 -0
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +136 -7
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +35 -2
package/adapters/opencode.js
CHANGED
|
@@ -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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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)
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
439
|
+
"[Ultra context spine]",
|
|
414
440
|
\`Project: \${active.root}\`,
|
|
415
441
|
\`Change: \${change.id} (\${change.kind ?? "unknown"}, \${change.status ?? "active"})\`,
|
|
416
|
-
\`
|
|
417
|
-
\`
|
|
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;
|
|
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
|
|
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,
|
|
235
|
-
assert.equal(upstreamSpec.tools.length,
|
|
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, /
|
|
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
|
|
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'), /
|
|
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');
|