ultra-builder-pro-cli 0.14.0 → 0.16.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/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +63 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +140 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +228 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
|
|
10
|
-
const { createChange } = require('../../mcp-server/lib/change-workflow.cjs');
|
|
11
|
-
const { createTask } = require('../../mcp-server/lib/state-ops.cjs');
|
|
12
|
-
const { compileRoleContext } = require('../../mcp-server/lib/context-spine.cjs');
|
|
13
|
-
|
|
14
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
15
|
-
const ADAPTER = path.join(REPO_ROOT, 'hooks', 'adapters', 'kimi.py');
|
|
16
|
-
|
|
17
|
-
function run(feature, payload, args = []) {
|
|
18
|
-
return spawnSync('python3', [ADAPTER, feature, ...args], {
|
|
19
|
-
cwd: REPO_ROOT,
|
|
20
|
-
input: JSON.stringify(payload),
|
|
21
|
-
encoding: 'utf8',
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function seedContext(project, taskId = 'task-7') {
|
|
26
|
-
const state = initStateDb(path.join(project, '.ultra', 'state.db'));
|
|
27
|
-
state.db.prepare(
|
|
28
|
-
`INSERT INTO baselines
|
|
29
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
30
|
-
VALUES ('baseline', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
|
|
31
|
-
).run(new Date().toISOString());
|
|
32
|
-
const { change } = createChange(state.db, {
|
|
33
|
-
id: 'hook-change', title: 'Hook change', kind: 'quick',
|
|
34
|
-
intent: 'Inject the authoritative task.',
|
|
35
|
-
docs_impact: { status: 'none', rationale: 'fixture' },
|
|
36
|
-
}, { rootDir: project });
|
|
37
|
-
const task = createTask(state.db, {
|
|
38
|
-
id: taskId, title: 'Hook task', type: 'bugfix', priority: 'P0', change_id: change.id,
|
|
39
|
-
});
|
|
40
|
-
compileRoleContext(state.db, {
|
|
41
|
-
input: {
|
|
42
|
-
task_id: task.id, role: 'implement', gate: 'implementation',
|
|
43
|
-
execution_contract: {
|
|
44
|
-
slice_kind: 'tracer_bullet', public_seam: 'hook injection',
|
|
45
|
-
verification_command: 'node --test adapters/tests/kimi-hook.test.cjs',
|
|
46
|
-
},
|
|
47
|
-
next_action: 'Continue the authoritative hook task.',
|
|
48
|
-
},
|
|
49
|
-
change, tasks: [task], rootDir: project,
|
|
50
|
-
});
|
|
51
|
-
closeStateDb(state.db);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
test('Kimi hook adapter rejects non-workflow policy hooks without blocking the host', () => {
|
|
55
|
-
const result = run('block_dangerous_commands.py', {
|
|
56
|
-
session_id: 'session-1',
|
|
57
|
-
cwd: REPO_ROOT,
|
|
58
|
-
hook_event_name: 'PreToolUse',
|
|
59
|
-
tool_name: 'Shell',
|
|
60
|
-
tool_input: { command: 'git reset --hard' },
|
|
61
|
-
});
|
|
62
|
-
assert.equal(result.status, 0, result.stderr);
|
|
63
|
-
assert.match(JSON.parse(result.stdout).message, /refused unknown feature/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('Kimi hook adapter translates active edit context to the native message field', () => {
|
|
67
|
-
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-'));
|
|
68
|
-
try {
|
|
69
|
-
seedContext(project);
|
|
70
|
-
const result = run('active_task_context.py', {
|
|
71
|
-
session_id: 'session-2',
|
|
72
|
-
cwd: project,
|
|
73
|
-
hook_event_name: 'PreToolUse',
|
|
74
|
-
tool_name: 'Edit',
|
|
75
|
-
tool_input: { file_path: 'src/a.py' },
|
|
76
|
-
});
|
|
77
|
-
assert.equal(result.status, 0, result.stderr);
|
|
78
|
-
const output = JSON.parse(result.stdout);
|
|
79
|
-
assert.match(output.message, /task-7/);
|
|
80
|
-
assert.equal(output.hookSpecificOutput, undefined);
|
|
81
|
-
} finally {
|
|
82
|
-
fs.rmSync(project, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('Kimi hook adapter emits the native deny contract for projection writes', () => {
|
|
87
|
-
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-hook-deny-'));
|
|
88
|
-
try {
|
|
89
|
-
fs.mkdirSync(path.join(project, '.ultra'));
|
|
90
|
-
const result = run('active_task_context.py', {
|
|
91
|
-
session_id: 'session-deny',
|
|
92
|
-
cwd: project,
|
|
93
|
-
hook_event_name: 'PreToolUse',
|
|
94
|
-
tool_name: 'Write',
|
|
95
|
-
tool_input: { file_path: '.ultra/tasks/tasks.json' },
|
|
96
|
-
});
|
|
97
|
-
assert.equal(result.status, 0, result.stderr);
|
|
98
|
-
const output = JSON.parse(result.stdout);
|
|
99
|
-
assert.equal(output.hookSpecificOutput.permissionDecision, 'deny');
|
|
100
|
-
assert.match(output.hookSpecificOutput.permissionDecisionReason, /\.ultra\/state\.db/);
|
|
101
|
-
} finally {
|
|
102
|
-
fs.rmSync(project, { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('Kimi subagent fields are normalized for the existing lifecycle tracker', () => {
|
|
107
|
-
const script = [
|
|
108
|
-
'import importlib.util, json, pathlib',
|
|
109
|
-
`p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
|
|
110
|
-
's = importlib.util.spec_from_file_location("kimi_hook_adapter", p)',
|
|
111
|
-
'm = importlib.util.module_from_spec(s)',
|
|
112
|
-
's.loader.exec_module(m)',
|
|
113
|
-
'payload = {"hook_event_name":"SubagentStart","agent_name":"review-code","session_id":"s"}',
|
|
114
|
-
'print(json.dumps(m.normalize_input(payload)))',
|
|
115
|
-
].join('; ');
|
|
116
|
-
const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
117
|
-
assert.equal(result.status, 0, result.stderr);
|
|
118
|
-
assert.deepEqual(JSON.parse(result.stdout), {
|
|
119
|
-
hook_event_name: 'SubagentStart',
|
|
120
|
-
agent_name: 'review-code',
|
|
121
|
-
session_id: 's',
|
|
122
|
-
agent_id: 'review-code',
|
|
123
|
-
agent_type: 'review-code',
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('Kimi adapter preserves an explicit Stop deny and recovery context schema', () => {
|
|
128
|
-
const script = [
|
|
129
|
-
'import importlib.util, json, pathlib',
|
|
130
|
-
`p = pathlib.Path(${JSON.stringify(ADAPTER)})`,
|
|
131
|
-
's = importlib.util.spec_from_file_location("kimi_hook_adapter", p)',
|
|
132
|
-
'm = importlib.util.module_from_spec(s)',
|
|
133
|
-
's.loader.exec_module(m)',
|
|
134
|
-
'values = [m.adapt_output({"decision":"block","reason":"finish gates"}, "Stop"), m.adapt_output({"additionalContext":"restored"}, "PostCompact")]',
|
|
135
|
-
'print(json.dumps(values))',
|
|
136
|
-
].join('; ');
|
|
137
|
-
const result = spawnSync('python3', ['-c', script], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
138
|
-
assert.equal(result.status, 0, result.stderr);
|
|
139
|
-
assert.deepEqual(JSON.parse(result.stdout), [
|
|
140
|
-
{
|
|
141
|
-
hookSpecificOutput: {
|
|
142
|
-
message: 'finish gates',
|
|
143
|
-
permissionDecision: 'deny',
|
|
144
|
-
permissionDecisionReason: 'finish gates',
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{ message: 'restored' },
|
|
148
|
-
]);
|
|
149
|
-
});
|
|
@@ -1,259 +0,0 @@
|
|
|
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`/);
|
|
125
|
-
assert.doesNotMatch(learn, /_unverified|learned-[^\s/]*-unverified/i);
|
|
126
|
-
assert.match(review, /Kimi `AgentSwarm`/);
|
|
127
|
-
assert.match(review, /\$KIMI_PLUGIN_ROOT\/agents\//);
|
|
128
|
-
assert.match(review, /scripts\/review_wait\.py/);
|
|
129
|
-
assert.match(review, /spec_fidelity/);
|
|
130
|
-
assert.match(review, /engineering_standards/);
|
|
131
|
-
assert.doesNotMatch(review, /background mode|run_in_background/);
|
|
132
|
-
assert.match(init, /task\.init_project/);
|
|
133
|
-
assert.doesNotMatch(init, /Claude Code|OpenCode|Codex/);
|
|
134
|
-
assert.match(bootstrap, /\.ultra\/state\.db/);
|
|
135
|
-
assert.match(bootstrap, /external providers/);
|
|
136
|
-
assert.match(fs.readFileSync(path.join(pluginRoot, 'skills', 'codex-collab', 'SKILL.md'), 'utf8'), /--ephemeral/);
|
|
137
|
-
|
|
138
|
-
const allPromptAssets = [];
|
|
139
|
-
for (const root of ['commands', 'skills', 'agents']) {
|
|
140
|
-
const pending = [path.join(pluginRoot, root)];
|
|
141
|
-
while (pending.length) {
|
|
142
|
-
const current = pending.pop();
|
|
143
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
144
|
-
const file = path.join(current, entry.name);
|
|
145
|
-
if (entry.isDirectory()) pending.push(file);
|
|
146
|
-
else if (entry.isFile() && /\.(?:md|json)$/.test(entry.name)) allPromptAssets.push(file);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const foreign = /~\/.claude|~\/.codex|~\/.config\/opencode|CLAUDE\.md|TaskCreate|TaskUpdate|TaskList|AskUserQuestion|run_in_background:\s*true|\$CLAUDE_PLUGIN_ROOT|Kimi `Shell`/;
|
|
151
|
-
for (const file of allPromptAssets) {
|
|
152
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
153
|
-
assert.doesNotMatch(text, foreign, file);
|
|
154
|
-
assert.doesNotMatch(text, /(^|[\s`("'])\/ultra-(?!builder-pro:)[a-z]/m, file);
|
|
155
|
-
assert.doesNotMatch(text, /[\u3400-\u9fff]|ultra-review-findings-v1|Context7|Exa MCP|confidence\s*>=?\s*\d+/iu, 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
|
-
});
|