Rules for Assistants

Alberto BasaloAlberto Basalo
8 min read

The importance of the 'what' as much as the 'how' is paramount. I know it sounds like a tongue twister, but I'll try to explain it. When you ask an assistant to generate code for you, you must be as specific as possible so that it does what you want. So far, so good.

However, when it comes to programming, there is still a lot of freedom to choose the language, paradigm, structure, conventions, etc. Therefore, it's essential to guide and restrict the options. This is a way to add context that ensures a response closer to what you desire.

You can provide this context in each prompt or configure it so the assistant knows what you're talking about. It will depend a bit on the tool you use, but in this post, we will see how to do it in Cursor and with GitHub Copilot.

General rules

If all your projects share similar characteristics, you can set those rules once in the assistant. But more likely, you will need to configure it for each project.

Let's see how these tools handle rules, and then we'll see how to configure them.

General rules in Cursor

Cursor was the first to add this functionality, and did it in a very simple way. Rules were set in the Settings tab of Cursor, in a text box that did not invite uncontrolled growth.

The feeling of the developer community is that those rules were very similar to the concept of system prompt of AI. To this regard, I have already dedicated a post: System prompt, configura tu asistente.

I summarize it and define a general context that can serve any project. At the same time, do not forget that it is a context, with its weight in characters; I mean, in tokens. I go to the most practical and follow the example of Íñigo Montoya in The Princess Promised:

I am a senior software engineer who works as a freelance consultant and trainer for programmers.

I write clean, tested, and well-documented code using appropriate design patterns and software architectures for the size of the project.

Act as an experienced specialist with a deep knowledge of programming languages and technologies.

Help me develop clear and well-documented examples and tutorials and make corrections and code reviews for my students and clients.

For this, comply with the following instructions:

- Answer me in English, even if you ask me in Spanish.
- Be concise and direct, without preambles or polite farewells.
- Do not explain fundamentals or basic concepts.
- Before answering, read the whole question carefully, prepare a response, evaluate, correct, and then answer.
- If you do not know the answer, do not invent one; ask me so we can find it together.
- Complete all tasks assigned to you without leaving anything undone.
- Adjust to the specific rules of each project, or use the standards or best practices you have available.

General rules in GitHub Copilot

GitHub Copilot does not distinguish between general rules and project-specific rules, at least not in a direct way. The instructions, as Copilot calls them, are set in files, which you must create in your project.

And that's how we go, to create instruction files for our projects, both with Cursor and with GitHub Copilot.

Project-specific rules

As I said, both tools allow us to create special files that will be used as context for our prompts.

Project-specific rules in Cursor

The rules file in Cursor is completely determined, it is called .cursorrules, and is saved in the root of your project. Point, they are Cursor rules.

No where it specifies its format, but it is assumed to be a plain text file, and to facilitate its human reading, the Markdown format is usually used.

Project-specific rules in GitHub Copilot

In Copilot, there is no specific rules file, but several! Yes, it is a bit surprising, but at the moment of writing this post, it is a feature that appears as preview or experimental. So I guess they want to see how and how much these options are used.

In principle, it forces you to create a copilot-instructions.md file in the special .github folder. So we have a custom agreement not configurable for this tool again. That said, it also leaves no room for doubt about its content; it must be a Markdown file.

But there are more files. The above applies to general code generation. And it turns out that Copilot allows you to give specific instructions to generate code, but also to code reviews or even testing.

In these cases, you can indicate the instructions as a brief text, or as a file in your project. Which leaves open the possibility of using any file in your project as context.

As I tell you, these options are marked as experimental, so it is not surprising that they are not very clear, are duplicated and can change or disappear at any time. So we will use only what seems more stable, the file .github/copilot-instructions.md.

Examples of project-specific rules

The guides that the tools provide to establish the instructions are quite generic, so it is not easy to give general advice. Based on my experience and the examples I have seen, I summarize the most important ones:

1 - The size matters. Keep your instructions as short as possible.

2 - The context is the king. Restrict the freedom of the AI to use your preferences.

3 - An example is better than a thousand words.

Example for a Node Express project with TypeScript

# Dependencies and versions

- Node 22
- Express 5
- TypeScript 5
- ESLint 9
- Prettier 3
- Jest 29

# Style and best practices rules

- Add JSDoc to all public members of classes and all exports.
- Use the `#` prefix for private members.
- Add guards to methods that receive data from external sources.
- Do not use `null` as a value, and define default values for data structures.
- Do not use `any` as a type, and always define specific types.
- Avoid the use of `enum` and use `type` to represent value domains.
- Use `interface` to define object behaviors.
- Do not use magic numbers and define constants for values.
- Do not nest repetitive or conditional structures; use helper functions.
- Keep functions and methods simple, with less than 20 instructions.

# File and folder structure

- Each file exports a single element, with the convention `name.type.ts`.
- The file types are: `controller`, `dto`, `entity`, `middleware`, `repository`, `service`, `type`, `util`.
- There are three root folders: `api`, `middleware` and `shared`.

# Rules for testing

- Use Jest to write tests.
- Write a test for each public method or function.
- Write tests for all API routes.
- Follow the `Arrange-Act-Assert` convention to organize the test code.
- Group tests in `describe` blocks for the same file or module.
- Use `beforeAll` to initialize shared variables or contexts between tests.
- Name the test variables using the convention `inputName`, `mockName`, `actualName`, `expectedName`.
- Use realistic data and reuse it in multiple tests.

# Example

```typescript
// src/models/user.model.ts

/**
 * Modelo de usuario
 */
export interface User {
  id: string | undefined;
  name: string;
  email: string;
}

// src/models/user-token.model.ts

/**
 * User token model
 */
export interface UserToken {
  userId: string;
  token: string;
}

// src/controllers/user.controller.ts
/**
 * User controller
 * @requires UserService
 */
export class UserController {
  #userService: UserService = new UserService();

  /**
   * Create a new user
   * @param user - User to create
   * @returns User token created
   * @throws Error if the name or email is not valid
   */
  postNewUser(user: User): UserToken {
    if (!user.name || !user.email) {
      throw new Error("Name and email are required");
    }
    return this.#userService.createUser(user);
  }
}

// src/services/user.service.ts

/**
 * User service
 * @requires UserRepository
 */
export class UserService {
  #userRepository: UserRepository = new UserRepository();

  createUser(user: User): UserToken {
    const userId = this.#userRepository.saveUser(user);
    const token = generateToken(userId);
    return { userId, token };
  }
}
// src/repositories/user.repository.ts

/**
 * User repository
 * - Saves users in memory
 */
export class UserRepository {
  #users: User[] = [];
  #STRING_BASE = 36;
  #ID_PREFIX = 2;
  #ID_LENGTH = 15;

  /**
   * Save a user in memory
   * @param user - User to save
   * @returns Saved user identifier
   */
  saveUser(user: User): string {
    const id = this.#generateId();
    user.id = id;
    this.#users.push(user);
    return id;
  }

  #generateId(): string {
    return Math.random().toString(this.#STRING_BASE).substring(this.#ID_PREFIX, this.#ID_LENGTH);
  }
}

// src/shared/utils/token.util.ts

/**
 * Utility to generate tokens
 * @param userId - User identifier
 * @returns Generated token
 */
export function generateToken(userId: string): string {
  const random = Math.random();
  return `${Date.now()}_${random}_${userId}`;
}
```

Priority maintainable, robust, tested, and documented code.

Takeaway

As you can see, project instructions are essential to control the behavior of the AI. But do not forget that they are instructions and that the AI does not always interpret them as you expect, word for word. So don't stay with a single experience, but as always with the AI, test and evaluate.

To start, there are a couple of places where you can find inspiration:

Although at first glance, they seem to be aimed at Cursor, you can use them equally for GitHub Copilot. Remember in which path they go in each case:

  • .cursorrules for Cursor
  • .github/copilot-instructions.md para GitHub Copilot

As always, I hope this post has been useful to you and helps you to comply with the AIDDbot motto:

code smarter, not harder

0
Subscribe to my newsletter

Read articles from Alberto Basalo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alberto Basalo
Alberto Basalo

I am a full-stack developer with over 25 years of experience. I can help you with: Angular and Nest architecture. Testing with Cypress. Cleaning your code. AI Drive Development.