|
| 1 | +Read the work-in-progress specs in the specs folder. |
| 2 | + |
| 3 | +We'll be expanding the CLI spec with some new commands: |
| 4 | + |
| 5 | +* aw repo init [--vcs=git|hg|etc] [--devenv=no|nix|spack|bazel|etc] [--devcontainer=yes|no] [--direnv=yes|no] [--task-runner=just|make|etc] [--supported-agents=all|codex|claude|etc] [optional-project-description-string] |
| 6 | + |
| 7 | +When the project description is not provided. We launch an editor in the standard configurable way and the user enters the project description there |
| 8 | +The aw tool then launches a local agent to initialize the repo. The standard command-line and configuration settings control which agent is used and how it is launched. |
| 9 | + |
| 10 | +git is the default version control system. |
| 11 | +just is the default task runner. |
| 12 | + |
| 13 | +--devenv defaults to nix. "none" is a valid alias of "no" |
| 14 | +--devcontainer is enabled by default |
| 15 | +--direnv is enabled by default |
| 16 | +--supported-agents is set to all by default. |
| 17 | + |
| 18 | +The selected command-line options and project description are combined into a suitable task prompt that is handed off to the agent in conversational mode. |
| 19 | +The prompt suggests to the agent to collect any additional details for initializing the repo from the user. The agent is asked to suggest the use of specific testing frameworks and linters. The instructions tell the agent that if the user approves these choices, at the end of the conversation the agent should create an AGENTS.md file providing the instructions for running the tests and lints using the selected task runner. The init command completes by creating symlinks for all supported agents (see the `aw repo instructions link` command below which executes the same step in an already initialized repo) |
| 20 | + |
| 21 | +* aw repo instructions create |
| 22 | + |
| 23 | +Uses a similar process to the init command above, but the agent is additionally prompted to review the repo before collecting information from the user. |
| 24 | + |
| 25 | +* aw repo instructions link [--supported-agents=all|codex|etc] [source-file] |
| 26 | + |
| 27 | +This command will have behavior similar to the following ruby program: |
| 28 | + |
| 29 | +``` |
| 30 | +#!/usr/bin/env ruby |
| 31 | +# frozen_string_literal: true |
| 32 | +# |
| 33 | +# sync_agent_links.rb |
| 34 | +# |
| 35 | +# Create relative symlinks from various AI agent "rules" files to AGENTS.md, |
| 36 | +# and add them to git. Supports selecting which agents to target. |
| 37 | +# |
| 38 | +# Examples: |
| 39 | +# ruby sync_agent_links.rb # all agents (default) |
| 40 | +# ruby sync_agent_links.rb --agents=copilot,claude # only those agents |
| 41 | +# ruby sync_agent_links.rb --force # overwrite conflicts |
| 42 | +# ruby sync_agent_links.rb --dry-run # preview actions |
| 43 | +# |
| 44 | +# Notes: |
| 45 | +# - Run from anywhere inside the repo (it will cd to repo root). |
| 46 | +# - On Windows, create symlinks via Git Bash or WSL; native symlinks may require admin. |
| 47 | + |
| 48 | +require "optparse" |
| 49 | +require "pathname" |
| 50 | +require "fileutils" |
| 51 | +require "open3" |
| 52 | + |
| 53 | +# ---------- Agent → link targets (relative to repo root) ---------- |
| 54 | +AGENT_LINKS = { |
| 55 | + # Generic catch-alls commonly read by multiple tools |
| 56 | + "generic" => ["AGENT.md", "README.agent.md", ".rules"], |
| 57 | + |
| 58 | + # Specific tools |
| 59 | + "copilot" => [".github/copilot-instructions.md", |
| 60 | + ".github/instructions/AGENTS.instructions.md"], |
| 61 | + "claude" => ["CLAUDE.md"], |
| 62 | + "gemini" => ["GEMINI.md"], |
| 63 | + "cursor" => [".cursor/rules/AGENTS.mdc", ".cursorrules"], |
| 64 | + "windsurf" => [".windsurf/rules/AGENTS.md", ".windsurfrules"], |
| 65 | + "zed" => [".rules"], # Zed reads several names; .rules is its native one |
| 66 | + "cline" => [".clinerules"], |
| 67 | + "roo" => [".roorules"], |
| 68 | + "jetbrains" => [".aiassistant/rules/AGENTS.md", ".junie/guidelines.md"], |
| 69 | + "openhands" => [".openhands/microagents/repo.md"], |
| 70 | +}.freeze |
| 71 | + |
| 72 | +VALID_AGENT_NAMES = AGENT_LINKS.keys.sort.freeze |
| 73 | + |
| 74 | +# ---------- CLI options ---------- |
| 75 | +opts = { |
| 76 | + force: false, |
| 77 | + dry_run: false, |
| 78 | + agents: nil, # nil means "all" |
| 79 | +} |
| 80 | + |
| 81 | +parser = OptionParser.new do |o| |
| 82 | + o.banner = "Usage: ruby sync_agent_links.rb [--agents=a,b,c] [--force] [--dry-run]" |
| 83 | + |
| 84 | + o.on("--agents=LIST", "Comma-separated agent keys (default: all).", |
| 85 | + "Valid: #{VALID_AGENT_NAMES.join(', ')}") do |list| |
| 86 | + opts[:agents] = list.split(",").map(&:strip).reject(&:empty?) |
| 87 | + end |
| 88 | + |
| 89 | + o.on("--force", "Overwrite existing files/symlinks") { opts[:force] = true } |
| 90 | + o.on("--dry-run", "Show actions without changing anything") { opts[:dry_run] = true } |
| 91 | + o.on("-h", "--help", "Show this help") { puts o; exit 0 } |
| 92 | +end |
| 93 | + |
| 94 | +begin |
| 95 | + parser.parse! |
| 96 | +rescue OptionParser::ParseError => e |
| 97 | + warn e.message |
| 98 | + warn parser |
| 99 | + exit 1 |
| 100 | +end |
| 101 | + |
| 102 | +# ---------- Helpers ---------- |
| 103 | +def run!(cmd_ary, dry:) |
| 104 | + if dry |
| 105 | + puts "[dry-run] #{cmd_ary.join(' ')}" |
| 106 | + true |
| 107 | + else |
| 108 | + system(*cmd_ary) |
| 109 | + end |
| 110 | +end |
| 111 | + |
| 112 | +def inside_git_repo? |
| 113 | + system("git", "rev-parse", "--is-inside-work-tree", out: File::NULL, err: File::NULL) |
| 114 | +end |
| 115 | + |
| 116 | +def git_root |
| 117 | + out, st = Open3.capture2("git", "rev-parse", "--show-toplevel") |
| 118 | + st.success? ? out.strip : nil |
| 119 | +end |
| 120 | + |
| 121 | +def relpath(from_dir_abs, target_abs) |
| 122 | + Pathname.new(target_abs).relative_path_from(Pathname.new(from_dir_abs)).to_s |
| 123 | +end |
| 124 | + |
| 125 | +# ---------- Preconditions ---------- |
| 126 | +unless inside_git_repo? |
| 127 | + warn "Error: must run inside a git repository." |
| 128 | + exit 1 |
| 129 | +end |
| 130 | + |
| 131 | +repo_root = git_root |
| 132 | +if repo_root.nil? || repo_root.empty? |
| 133 | + warn "Error: unable to determine git repo root." |
| 134 | + exit 1 |
| 135 | +end |
| 136 | + |
| 137 | +agents_file = File.join(repo_root, "AGENTS.md") |
| 138 | +unless File.file?(agents_file) |
| 139 | + warn "Error: #{agents_file} not found. Create it first." |
| 140 | + exit 1 |
| 141 | +end |
| 142 | + |
| 143 | +Dir.chdir(repo_root) |
| 144 | + |
| 145 | +# Determine which agents to act on |
| 146 | +selected_agents = |
| 147 | + if opts[:agents].nil? || opts[:agents].empty? || opts[:agents].include?("all") |
| 148 | + VALID_AGENT_NAMES |
| 149 | + else |
| 150 | + unknown = opts[:agents] - VALID_AGENT_NAMES |
| 151 | + unless unknown.empty? |
| 152 | + warn "Unknown agent(s): #{unknown.join(', ')}" |
| 153 | + warn "Valid agents: #{VALID_AGENT_NAMES.join(', ')}" |
| 154 | + exit 1 |
| 155 | + end |
| 156 | + opts[:agents] |
| 157 | + end |
| 158 | + |
| 159 | +# Build link target list |
| 160 | +links = selected_agents.flat_map { |k| AGENT_LINKS[k] }.uniq |
| 161 | + |
| 162 | +puts "Repo: #{repo_root}" |
| 163 | +puts "AGENTS.md: #{agents_file}" |
| 164 | +puts "Agents: #{selected_agents.join(', ')}" |
| 165 | +puts "Links to create: #{links.size}" |
| 166 | +puts |
| 167 | + |
| 168 | +created = 0 |
| 169 | +skipped = 0 |
| 170 | +git_added = 0 |
| 171 | + |
| 172 | +links.each do |link_rel| |
| 173 | + link_path = File.join(repo_root, link_rel) |
| 174 | + link_dir = File.dirname(link_path) |
| 175 | + |
| 176 | + # Ensure parent directory exists |
| 177 | + unless Dir.exist?(link_dir) |
| 178 | + if opts[:dry_run] |
| 179 | + puts "[dry-run] mkdir -p #{link_dir}" |
| 180 | + else |
| 181 | + FileUtils.mkdir_p(link_dir) |
| 182 | + end |
| 183 | + end |
| 184 | + |
| 185 | + # Compute relative target path from link's directory to AGENTS.md |
| 186 | + rel_to_agents = relpath(link_dir, agents_file) |
| 187 | + |
| 188 | + # Handle existing files/symlinks |
| 189 | + if File.symlink?(link_path) |
| 190 | + current = File.readlink(link_path) rescue nil |
| 191 | + if current == rel_to_agents && !opts[:force] |
| 192 | + puts "OK (already linked): #{link_rel} -> #{current}" |
| 193 | + skipped += 1 |
| 194 | + next |
| 195 | + end |
| 196 | + |
| 197 | + if opts[:force] |
| 198 | + puts "Rewriting symlink: #{link_rel}" |
| 199 | + if opts[:dry_run] |
| 200 | + puts "[dry-run] rm -f #{link_rel}" |
| 201 | + else |
| 202 | + FileUtils.rm_f(link_path) |
| 203 | + end |
| 204 | + else |
| 205 | + puts "Skip (different symlink; use --force): #{link_rel} -> #{current}" |
| 206 | + skipped += 1 |
| 207 | + next |
| 208 | + end |
| 209 | + elsif File.exist?(link_path) |
| 210 | + if opts[:force] |
| 211 | + puts "Replacing file: #{link_rel}" |
| 212 | + if opts[:dry_run] |
| 213 | + puts "[dry-run] rm -f #{link_rel}" |
| 214 | + else |
| 215 | + FileUtils.rm_f(link_path) |
| 216 | + end |
| 217 | + else |
| 218 | + puts "Skip (exists and not a symlink): #{link_rel}" |
| 219 | + skipped += 1 |
| 220 | + next |
| 221 | + end |
| 222 | + end |
| 223 | + |
| 224 | + # Create symlink (relative) |
| 225 | + if opts[:dry_run] |
| 226 | + puts "[dry-run] ln -s #{rel_to_agents} #{link_rel}" |
| 227 | + else |
| 228 | + File.symlink(rel_to_agents, link_path) |
| 229 | + end |
| 230 | + created += 1 |
| 231 | + |
| 232 | + # git add -f |
| 233 | + if run!(["git", "add", "-f", link_rel], dry: opts[:dry_run]) |
| 234 | + git_added += 1 |
| 235 | + end |
| 236 | +end |
| 237 | + |
| 238 | +puts |
| 239 | +puts "Done." |
| 240 | +puts "Created: #{created} Skipped: #{skipped} Git-added: #{git_added}" |
| 241 | +puts "(dry-run: nothing actually changed)" if opts[:dry_run] |
| 242 | + |
| 243 | +``` |
| 244 | + |
| 245 | +When a source-file is not provided, the command will look for an existing instructions file (for one of the supported agents). |
| 246 | +If there is a single existing instructions file, it would be assumed to be the source file. |
| 247 | + |
| 248 | +* aw repo check [--supported-agents=...] |
| 249 | + |
| 250 | +This command will perform various checks: |
| 251 | + |
| 252 | +* Are there instruction files? Print information about the missing ones if there is disagreement with the supported-agents configuration value (remember, this configuration value can be supplied on the command-line here, but it may also appear in a configuration file within the repo, in the user home folder, on the system, etc). |
| 253 | + |
| 254 | +* Is there a devcontainer setup? Does its own health check procedure pass (please document that such procedure will be available in the devcontainer spec) |
| 255 | + |
| 256 | +* aw health [--supported-agents=...] |
| 257 | + |
| 258 | +Performs diagnostic health checks for the presence and login status of the configured agentic tools. |
| 259 | + |
| 260 | +Don't write any code. We are currently working on the spec. |
| 261 | + |
| 262 | +Create a precise copy of these instructions in the .agents/task folder as the `agent-task` tool usually does. |
0 commit comments