Last active
January 23, 2026 14:22
-
-
Save jbilbo/922ed82e6abd412651b61e68e40b5215 to your computer and use it in GitHub Desktop.
AgentsUnifier - Utility to consolidate AI agent configuration files into AGENTS.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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