Skip to content

Instantly share code, notes, and snippets.

@jbilbo
Last active January 23, 2026 14:22
Show Gist options
  • Select an option

  • Save jbilbo/922ed82e6abd412651b61e68e40b5215 to your computer and use it in GitHub Desktop.

Select an option

Save jbilbo/922ed82e6abd412651b61e68e40b5215 to your computer and use it in GitHub Desktop.
AgentsUnifier - Utility to consolidate AI agent configuration files into AGENTS.md
#!/usr/bin/env -S RBENV_VERSION=system ruby
# frozen_string_literal: true
require 'fileutils'
require 'pathname'
class AgentsUnifier
SOURCE_FILES = [
'GEMINI.md',
'CLAUDE.md',
File.join('.github', 'copilot-instructions.md')
].freeze
def initialize(project_root)
@project_root = Pathname.new(project_root)
@actions = []
@warnings = []
@source_sections = []
end
def run
check_git_status
gather_sources
validate_migration_safety
ensure_agents_file
cleanup_old_files
create_claude_symlink
print_summary
end
private
attr_reader :project_root
def check_git_status
return if project_root.join('.git').exist?
puts "\nWARNING: This project is not under Git version control."
puts 'Ensure you have backups before proceeding.'
exit 1 unless ask_confirmation
end
def ask_confirmation
print 'Do you want to continue anyway? (yes/no): '
response = $stdin.gets&.strip&.downcase
%w[yes y].include?(response)
end
def gather_sources
SOURCE_FILES.each do |relative_path|
path = project_root.join(relative_path)
next unless path.file? && !path.symlink?
content = safe_read(path)
next if content.nil? || content.strip.empty?
@source_sections << { label: relative_path, content: content.strip }
end
end
def validate_migration_safety
agents_path = project_root.join('AGENTS.md')
if agents_path.file?
content = safe_read(agents_path)
if content && !content.strip.empty?
abort_with_error(
"AGENTS.md already has content. Manual intervention required."
)
end
end
return if @source_sections.length <= 1
files = @source_sections.map { |s| s[:label] }.join(', ')
abort_with_error(
"Multiple source files have content: #{files}\n" \
"Please consolidate content manually first."
)
end
def abort_with_error(message)
puts "\nABORTED: #{message}"
exit 1
end
def ensure_agents_file
path = project_root.join('AGENTS.md')
if path.file?
existing = safe_read(path)
if existing.nil?
@warnings << "Unable to read existing AGENTS.md; skipped."
elsif existing.strip.empty? && !@source_sections.empty?
write_file(path, build_agents_content, 'Populated AGENTS.md with migrated instructions')
else
@actions << 'AGENTS.md already present'
end
return
end
write_file(path, build_agents_content, 'Created AGENTS.md')
end
def build_agents_content
header = "# Project Instructions\n\n"
return header + "_Add shared instructions here._\n" if @source_sections.empty?
source = @source_sections.first
header + "#{clean_content(source[:content])}\n"
end
def clean_content(content)
content
.gsub(/^#+ CLAUDE\.md\s*\n/, '')
.gsub('Claude Code (claude.ai/code)', 'AI agents')
.strip
end
def cleanup_old_files
SOURCE_FILES.each do |relative_path|
path = project_root.join(relative_path)
next unless path.exist? || path.symlink?
path.delete
@actions << "Removed #{relative_path}"
rescue StandardError => e
@warnings << "Failed to remove #{relative_path}: #{e.message}"
end
end
def create_claude_symlink
path = project_root.join('CLAUDE.md')
target = Pathname.new('AGENTS.md')
if path.symlink?
return if path.readlink == target
path.delete
end
FileUtils.ln_s(target, path)
@actions << 'Created CLAUDE.md -> AGENTS.md symlink'
rescue StandardError => e
@warnings << "Failed to create CLAUDE.md symlink: #{e.message}"
end
def safe_read(path)
return nil unless path.file?
path.read
rescue StandardError => e
@warnings << "Failed to read #{path}: #{e.message}"
nil
end
def write_file(path, content, action_message)
path.dirname.mkpath
path.write(content)
@actions << action_message
rescue StandardError => e
@warnings << "Failed to write #{path}: #{e.message}"
end
def print_summary
puts "Unified agent instructions in #{project_root}."
if @actions.empty?
puts '- No changes were necessary.'
else
@actions.each { |msg| puts "- #{msg}" }
end
return if @warnings.empty?
puts "\nWarnings:"
@warnings.each { |msg| puts "- #{msg}" }
end
end
AgentsUnifier.new(Dir.pwd).run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment