2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-10-03 17:21:20 +08:00

DEV: update various ai agent configurations (#34192)
Some checks are pending
Licenses / run (push) Waiting to run
Linting / run (push) Waiting to run
Publish Assets / publish-assets (push) Waiting to run
Tests / core backend (push) Waiting to run
Tests / plugins backend (push) Waiting to run
Tests / core frontend (Chrome) (push) Waiting to run
Tests / plugins frontend (push) Waiting to run
Tests / themes frontend (push) Waiting to run
Tests / core system (push) Waiting to run
Tests / plugins system (push) Waiting to run
Tests / themes system (push) Waiting to run
Tests / core frontend (Firefox ESR) (push) Waiting to run
Tests / core frontend (Firefox Evergreen) (push) Waiting to run
Tests / chat system (push) Waiting to run
Tests / merge (push) Blocked by required conditions

Shortened AI agent file, so we conserve tokens
Use symlinks for all agent files where needed to avoid round trips to
llm
Added config for Open AI Codex / Gemini and Cursor
This commit is contained in:
Sam 2025-08-11 10:08:41 +10:00 committed by GitHub
parent df03ef6d05
commit e6f9cde35e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 463 additions and 130 deletions

View file

@ -0,0 +1,7 @@
---
description: Always include our AI agents guide
globs:
alwaysApply: true
---

@ai-agents.md

View file

@ -0,0 +1 @@
AI-AGENTS.md

1
AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
AI-AGENTS.md

View file

@ -1,156 +1,96 @@
# AI Coding Agent Guide

This file contains project-specific instructions that any AI coding agent should read at the start of each conversation and maintain in memory throughout the entire interaction. IMPORTANT: Once this file has been read or updated, it MUST be loaded at the beginning of any new conversation to ensure awareness of communication requirements, custom tasks, etc.
Project-specific instructions for AI agents. MUST be loaded at conversation start.

## Default Mode
- Architect mode enabled by default: detailed analysis, patterns, trade-offs, architectural guidance
- Stop and ask for context if unable to write code meeting guidelines

- Architect mode should be enabled by default.
- Focus on providing detailed analysis, patterns, trade-offs, and architectural guidance.
- If you're unable to write code that fits these guidelines, stop and ask for additional context from the developer.
## Development Rules
Discourse is large with long history. Understand context before changes.

## Development Environment
### All Files
- Always lint changed files
- Make display strings translatable (use placeholders, not split strings)
- Create subagent to review changes against this file after completing tasks

Discourse is a large project with a long development history. Ensure you understand the context of any changes you're making before you start.

### General Rules
These rules apply to ALL files being changed.

- Always lint changed files.
- Always make display strings translatable.
- Avoid splitting display strings into pieces: use translation placeholders, or multiple translatable strings where appropriate.
- After completing a task, create a subagent to review the changes and ensure they conform to the instructions in this file, as well as the prompt(s) given.
### Toolset
- Use `pnpm` for JavaScript, `bundle` for Ruby
- Use helpers in bin over bundle exec (bin/rspec, bin/rake)

### JavaScript
- Don't create empty backing classes for template tag only components, unless specifically asked to.
- Use the FormKit library for creating forms and form inputs. FormKit is documented here: https://meta.discourse.org/t/discourse-toolkit-to-render-forms/326439 and defined in `app/assets/javascripts/discourse/app/form-kit`
- No empty backing classes for template-only components unless requested
- Use FormKit for forms: https://meta.discourse.org/t/discourse-toolkit-to-render-forms/326439 (`app/assets/javascripts/discourse/app/form-kit`)

### JavaScript Documentation
- Always add JSDocs for classes, methods, and members, except for:
- `@service` members
- constructors
- Always use multiline JSDoc format.
- For components:
- Specify the component name with `@component`.
- List the params. These can be found in `this.args` in the JS, or `@paramname` in the `<template>`.
- For methods:
- Don't add `@returns` for `@action` methods.
- Don't add `@type` for getters, document with `@returns`.
- For members:
- Specify the @type.
### JSDoc
- Required for classes, methods, members (except `@service` members, constructors)
- Multiline format only
- Components: `@component` name, list params (`this.args` or `@paramname`)
- Methods: no `@returns` for `@action`, use `@returns` for getters (not `@type`)
- Members: specify `@type`

## Writing Tests

### General Rules
- Don't write tests for functionality that is handled by classes/components/modules other than the specific one being tested.
- Don't write obvious tests (eg, testing that a string can contain unicode characters)

### Ruby Test Rules
- Use `fab!()` instead of `let()` wherever possible.
- We use system tests in rails for UI integration testing, which is documented at https://dev.discourse.org/t/systematic-system-specs/82525, and examples are in `spec/system`
- We use page objects in system specs, defined in `spec/system/page_objects`

### Command Reference

#### Testing
## Testing
- Don't test functionality handled by other classes/components
- Don't write obvious tests
- Ruby: use `fab!()` over `let()`, system tests for UI (`spec/system`), page objects (`spec/system/page_objects`)

### Commands
```bash
# Run all Ruby tests
bin/rspec
# Ruby tests
bin/rspec [spec/path/file_spec.rb[:123]]
LOAD_PLUGINS=1 bin/rspec # Plugin tests

# Run a specific Ruby test file
bin/rspec spec/path/to/file_spec.rb
# JavaScript tests
bin/rake qunit:test # RUN all non plugin tests
LOAD_PLUGINS=1 TARGET=all FILTER='fill filter here' bin/rake qunit:test # RUN specific tests based on filter

# Run a specific Ruby test by line number
bin/rspec spec/path/to/file_spec.rb:123
Exmaple filters JavaScript tests:

# Run JavaScript tests
bin/rake qunit:test
emoji-test.js
...
acceptance("Emoji" ..
test("cooked correctly")
...
Filter string is: "Acceptance: Emoji: cooked correctly"

# Run a specific JavaScript test module
pnpm ember exam --filter 'Module | Filter | goes-here'
user-test.js
...
module("Unit | Model | user" ..
test("staff")
...
Filter string is: "Unit | Model | user: staff"

# Linting
bin/lint path/to/file path/to/another/file
bin/lint --fix path/to/file path/to/another/file
bin/lint --fix --recent # Lint all recently changed files
```

#### Linting and Formatting

```bash
# Lint Ruby files
bundle exec rubocop path/to/file
bundle exec stree write Gemfile path/to/file

# Lint JavaScript/TypeScript files
pnpm lint:js path/to/file
pnpm lint:hbs path/to/file
pnpm lint:prettier path/to/file

# Lint CSS/SCSS files
pnpm lint:css path/to/file
```
ALWAYS lint any changes you make

## Site Settings
- Much of Discourse is configured by site settings. These are defined in `config/site_settings.yml` or `config/settings.yml` files.
- Site Setting functionality is defined in `lib/site_setting_extension.rb`
- Site settings are accessed with `SiteSetting.setting_name` in ruby and `siteSettings.setting_name` in JS, with the latter needing a `@service siteSettings` declaration in Ember components
- Configured in `config/site_settings.yml` or `config/settings.yml` for plugins
- Functionality in `lib/site_setting_extension.rb`
- Access: `SiteSetting.setting_name` (Ruby), `siteSettings.setting_name` (JS with `@service siteSettings`)

## Service objects
- We have a service framework which is useful to extract business logic you usually find in controllers (validating parameters, fetching models, validating permissions, etc.). Its not limited to controllers, though, and can be used anywhere.
- This is documented at https://meta.discourse.org/t/using-service-objects-in-discourse/333641
- Examples are found at `app/services` but ONLY for classes with include `Service::Base`
## Services
- Extract business logic (validation, models, permissions) from controllers
- https://meta.discourse.org/t/using-service-objects-in-discourse/333641
- Examples: `app/services` (only classes with `Service::Base`)

## Database & Performance
- ActiveRecord: use `includes()`/`preload()` (N+1), `find_each()`/`in_batches()` (large sets), `update_all`/`delete_all` (bulk), `exists?` over `present?`
- Migrations: rollback logic, `algorithm: :concurrently` for large tables, deprecate before removing columns
- Queries: use `explain`, specify columns, strategic indexing, `counter_cache` for counts

### ActiveRecord Best Practices
- Always use `includes()` or `preload()` to prevent N+1 queries when accessing associations
- Use `find_each()` or `in_batches()` for large dataset processing
- Prefer database-level operations (`update_all`, `delete_all`) over Ruby loops for bulk changes
- Use `exists?` instead of `present?` when checking for record existence
## Security
- XSS: use `{{}}` (escaped) not `{{{ }}}`, sanitize with `sanitize`/`cook`, no `innerHTML`, careful with `@html`
- Auth: Guardian classes (`lib/guardian.rb`), POST/PUT/DELETE for state changes, CSRF tokens, `protect_from_forgery`
- Input: validate client+server, strong parameters, length limits, don't trust client-only validation
- Authorization: Guardian classes, route+action permissions, scope limiting, `can_see?`/`can_edit?` patterns

### Migration Guidelines
- Always include rollback logic in migrations
- Use `add_index(..., algorithm: :concurrently)` for large tables in production
- Never remove columns directly - deprecate first, then remove in subsequent release
- Test migrations on production-sized datasets when possible

### Query Optimization
- Use `explain` to analyze query performance during development
- Avoid `SELECT *` - specify needed columns explicitly
- Use database indexes strategically, but avoid over-indexing
- Consider using `counter_cache` for frequently accessed counts

## Security Guidelines

### XSS Prevention
- Always use `{{}}` (escaped) instead of `{{{ }}}` (unescaped) in Ember templates
- Sanitize user input using Discourse's built-in helpers (`sanitize`, `cook`)
- Never directly insert user content into `innerHTML` or similar DOM methods
- Use `@html` argument carefully and only with pre-sanitized content

- Always use Guardian classes for authorization checks, the Guardian class defined in lib/guardian.rb
- There are other Guardian classes defined in lib/guardian
- All state-changing requests must use POST/PUT/DELETE, never GET
- Ensure CSRF tokens are included in AJAX requests
- Use Rails' `protect_from_forgery` in controllers handling sensitive operations

### Input Sanitization
- Validate and sanitize all user inputs on both client and server side
- Use strong parameters in Rails controllers
- Apply appropriate length limits and format validation
- Never trust client-side validation alone

### Authorization
- Always use Guardian classes for authorization checks, the Guardian class defined in `lib/guardian.rb`
- There are other Guardian classes defined in `lib/guardian`
- Check permissions at both route and action levels
- Implement proper scope limiting (users should only see their own data)
- Use `can_see?` and `can_edit?` patterns consistently

## Knowledge Sharing and Persistence

- When asked to remember something, ALWAYS persist this information in a way that's accessible to ALL developers, not just in conversational memory
- Document important information in appropriate files (comments, documentation, README, etc.) so other developers (human or AI) can access it
- Information should be stored in a structured way that follows project conventions
- NEVER keep crucial information only in conversational memory - this creates knowledge silos
- If asked to implement something that won't be accessible to other users/developers in the repository, proactively highlight this issue
- The goal is complete knowledge sharing between ALL developers (human and AI) without exceptions
- When suggesting where to store information, recommend appropriate locations based on the type of information (code comments, documentation files, AI-AGENTS.md, etc.)
- Inform the developer when you detect a change in this file and have successfully reloaded it.
## Knowledge Sharing
- ALWAYS persist information for ALL developers (no conversational-only memory)
- Follow project conventions, prevent knowledge silos
- Recommend storage locations by info type
- Inform when this file changes and reloads

View file

@ -1 +0,0 @@
See @AI-AGENTS.md for all instructions.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AI-AGENTS.md

1
GEMINI.md Symbolic link
View file

@ -0,0 +1 @@
AI-AGENTS.md

374
bin/lint Executable file
View file

@ -0,0 +1,374 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "optparse"
require "json"
require "open3"
require "digest"

class Linter
BATCH_SIZE = 5000

LINTERS = {
rb: {
rubocop: {
check: "bundle exec rubocop --force-exclusion {files}",
fix: "bundle exec rubocop --force-exclusion -A {files}",
},
syntax_tree: {
check: "bundle exec stree check Gemfile {files}",
fix: "bundle exec stree write Gemfile {files}",
},
},
rake: {
syntax_tree: {
check: "bundle exec stree check {files}",
fix: "bundle exec stree write {files}",
},
},
js: {
eslint: {
check: "pnpm eslint --quiet {files}",
fix: "pnpm eslint --fix {files}",
},
prettier: {
check: "pnpm pprettier --list-different {files}",
fix: "pnpm pprettier --write {files}",
},
},
gjs: {
eslint: {
check: "pnpm eslint --quiet {files}",
fix: "pnpm eslint --fix {files}",
},
prettier: {
check: "pnpm pprettier --list-different {files}",
fix: "pnpm pprettier --write {files}",
},
ember_template_lint: {
check: "pnpm ember-template-lint {files}",
fix: "pnpm ember-template-lint --fix {files}",
},
},
hbs: {
ember_template_lint: {
check: "pnpm ember-template-lint {files}",
fix: "pnpm ember-template-lint --fix {files}",
},
prettier: {
check: "pnpm pprettier --list-different {files}",
fix: "pnpm pprettier --write {files}",
},
},
scss: {
stylelint: {
check: "pnpm stylelint {files}",
fix: "pnpm stylelint --fix {files}",
},
prettier: {
check: "pnpm pprettier --list-different {files}",
fix: "pnpm pprettier --write {files}",
},
},
css: {
prettier: {
check: "pnpm pprettier --list-different {files}",
fix: "pnpm pprettier --write {files}",
},
},
yml: {
yaml_syntax: {
check: "bundle exec yaml-lint {files}",
},
i18n_lint: {
check: "bundle exec ruby script/i18n_lint.rb {files}",
},
},
yaml: {
yaml_syntax: {
check: "bundle exec yaml-lint {files}",
},
},
}.freeze

def initialize(options = {})
@fix = options[:fix]
@recent = options[:recent]
@files = options[:files] || []
@verbose = options[:verbose]
@errors = []
@fixed = []
end

def run
files_to_lint = determine_files
return success_message if files_to_lint.empty?

# Phase 1: Group files by linter type and filter appropriately
linter_files = group_files_by_linter(files_to_lint)

# Phase 2: Process each linter's files in batches
linter_files.each do |linter_name, files|
next if files.empty?

files.each_slice(BATCH_SIZE) { |file_batch| run_linter_batch(linter_name, file_batch) }
end

print_summary
end

private

def determine_files
return recent_files if @recent
return @files unless @files.empty?

all_lintable_files
end

def recent_files
log_output, status = Open3.capture2("git", "log", "-50", "--name-only", "--pretty=format:")
return [] unless status.success?
log_files = log_output.lines.map(&:strip).reject(&:empty?)

tracked_out, tracked_status = Open3.capture2("git", "ls-files")
untracked_out, untracked_status =
Open3.capture2("git", "ls-files", "--others", "--exclude-standard")
return [] unless tracked_status.success? && untracked_status.success?

tracked = Set.new(tracked_out.lines.map(&:strip))
untracked = Set.new(untracked_out.lines.map(&:strip))

# Only keep log files that are still tracked, then add all untracked
candidates = Set.new
log_files.each { |f| candidates << f if tracked.include?(f) }
untracked.each { |f| candidates << f }

candidates.select { |f| lintable_file?(f) }
end

def all_lintable_files
tracked_out, tracked_status = Open3.capture2("git", "ls-files")
untracked_out, untracked_status =
Open3.capture2("git", "ls-files", "--others", "--exclude-standard")
return [] unless tracked_status.success? && untracked_status.success?

tracked = tracked_out.lines.map(&:strip)
untracked = untracked_out.lines.map(&:strip)

(tracked + untracked).select { |f| File.file?(f) && lintable_file?(f) }
end

def lintable_file?(file)
if file.include?("node_modules") || file.include?("vendor") || file.include?("tmp") ||
file.include?(".git") || file == "database.yml"
return false
end

ext = File.extname(file)[1..]

# Check for Ruby files in /bin/ directory without extensions
if ext.nil? || ext.empty?
if file.start_with?("bin/") && File.file?(file)
begin
first_line = File.open(file, &:readline)
return true if first_line.strip == "#!/usr/bin/env ruby"
rescue StandardError
# If we can't read the file, skip it
return false
end
end
return false
end

LINTERS.key?(ext.to_sym) || (file.include?("/assets/") && %w[js scss css hbs gjs].include?(ext))
end

def group_files_by_extension(files)
files
.group_by do |f|
ext = File.extname(f)[1..]
# Treat Ruby files in /bin/ without extensions as Ruby files
if (ext.nil? || ext.empty?) && f.start_with?("bin/") && File.file?(f)
begin
first_line = File.open(f, &:readline)
ext = "rb" if first_line.strip == "#!/usr/bin/env ruby"
rescue StandardError
# If we can't read the file, keep original extension
end
end
ext
end
.reject { |ext, _| ext.nil? || ext.empty? }
end

def group_files_by_linter(files)
linter_files = {}
grouped_files = group_files_by_extension(files)

grouped_files.each do |ext, ext_files|
linters = LINTERS[ext.to_sym]
next unless linters

linters.each do |linter_name, _commands|
filtered_files = filter_files_for_linter(linter_name, ext_files)
next if filtered_files.empty?

linter_files[linter_name] ||= []
linter_files[linter_name].concat(filtered_files)
end
end

# Remove duplicates that might occur if a file matches multiple linters
linter_files.each { |linter_name, file_list| linter_files[linter_name] = file_list.uniq }
linter_files
end

def run_linter_batch(linter_name, files)
# Find the command for this linter by looking through all file types
command = find_command_for_linter(linter_name)
return unless command

# Split command into parts and replace {files} placeholder
base_command = command.gsub("{files}", "").strip.split
cmd_args = base_command + files

puts "Running #{linter_name} on #{files.size} files..." if @verbose

if @fix
changed_files = detect_changed_files(files, cmd_args)
@fixed.concat(changed_files)
else
output, status = Open3.capture2e(*cmd_args)

unless status.success?
failed_files = parse_failed_files(linter_name, output, files)
@errors << { linter: linter_name, files: failed_files, output: output }
puts "#{linter_name} failed on: #{failed_files.join(", ")}" if @verbose
puts output unless output.strip.empty?
end
end
end

def find_command_for_linter(linter_name)
LINTERS.each do |_ext, linters|
linters.each do |name, commands|
return @fix ? commands[:fix] : commands[:check] if name == linter_name
end
end
nil
end

def detect_changed_files(files, cmd_args)
file_checksums = {}
files.each { |file| file_checksums[file] = file_checksum(file) }

Open3.capture2e(*cmd_args)

changed_files = []
files.each { |file| changed_files << file if file_checksum(file) != file_checksums[file] }

changed_files
end

def file_checksum(file)
return nil unless File.exist?(file)
Digest::MD5.file(file).hexdigest
end

def parse_failed_files(linter_name, output, all_files)
case linter_name
when :eslint
output.scan(%r{^(/[^\s:]+):\d+:\d+}).flatten.uniq
when :prettier
lines = output.lines.map(&:strip)
lines.select { |line| all_files.any? { |f| line.end_with?(File.basename(f)) } }
when :rubocop
output.scan(/^([^:]+):\d+:\d+/).flatten.uniq
when :ember_template_lint
output.scan(/^([^\s:]+):\d+:\d+/).flatten.uniq
when :stylelint
output.scan(/^([^\s:]+)\s+\d+:\d+/).flatten.uniq
when :syntax_tree
output.scan(/\[\d+m([^\[\]]+)\[0m/).flatten.uniq.select { |f| all_files.include?(f) }
when :yaml_syntax, :i18n_lint
all_files.select { |f| output.include?(f) }
else
all_files
end
end

def filter_files_for_linter(name, files)
case name
when :prettier, :ember_template_lint
files.select do |file|
file.include?("/assets/javascripts/") || file.include?("/plugins/") ||
file.include?("/themes/")
end
when :i18n_lint
files.select { |file| file.match?(%r{/(client|server)\..*\.yml$}) }
when :yaml_syntax
files.select { |file| !file.end_with?("database.yml") }
when :syntax_tree
# syntax_tree doesn't handle files without extensions well, skip for bin/ files
files.select { |file| !file.start_with?("bin/") || File.extname(file) != "" }
else
files
end
end

def print_summary
if @errors.empty?
if @fix && @fixed.any?
puts "✅ Fixed #{@fixed.size} files"
else
puts "✅ All files passed linting"
end
else
puts "❌ Linting failed on #{@errors.map { |e| e[:files] }.flatten.uniq.size} files"
exit 1
end
end

def success_message
puts "No files to lint"
end
end

def parse_options
options = {}

OptionParser
.new do |parser|
parser.banner = "Usage: bin/lint [options] [files...]"

parser.on("-h", "--help", "Show this help message") do
puts parser
puts
puts "Examples:"
puts " bin/lint # Lint all files"
puts " bin/lint --recent # Lint recently changed files"
puts " bin/lint --fix app.rb # Fix specific file"
puts " bin/lint app/models/*.rb # Lint multiple files"
exit
end

parser.on("-f", "--fix", "Attempt to automatically fix issues") { options[:fix] = true }

parser.on("-r", "--recent", "Lint recently changed files (last 50 commits)") do
options[:recent] = true
end

parser.on("-v", "--verbose", "Show verbose output") { options[:verbose] = true }
end
.parse!

options[:files] = ARGV unless ARGV.empty?
options
end

if __FILE__ == $0
options = parse_options
linter = Linter.new(options)
linter.run
end

View file

@ -38,6 +38,8 @@ class LocaleFileValidator
"The following keys use {{key}} instead of %{key} for interpolation keys:",
wrong_pluralization_keys:
"Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:",
invalid_file_format:
"The file is not a valid YAML format or does not contain a valid locale structure.",
invalid_one_keys:
"The following keys contain the number 1 instead of the interpolation key %{count}:",
}.merge(
@ -72,6 +74,9 @@ class LocaleFileValidator
validate_content(yaml)

@errors.any? { |_, value| value.any? }
rescue StandardError => e
@errors[:invalid_file_format] = ["Failed to parse #{@filename}: #{e.message}"]
true
end

def print_errors
@ -128,6 +133,10 @@ class LocaleFileValidator
end

def validate_pluralizations(yaml)
if !yaml.is_a?(Hash)
@errors[:wrong_pluralization_keys] << ["Root of the locale file must be a hash"]
return
end
each_pluralization(yaml) do |key, hash|
# ignore errors from some ActiveRecord messages
next if key.include?("messages.restrict_dependent_destroy")