Skip to content

Comments

Inline content in generated AGENTS.md with symlinks (#72)#74

Merged
lifeizhou-ap merged 1 commit intomainfrom
jonandersen/inline-agents-file
Feb 19, 2026
Merged

Inline content in generated AGENTS.md with symlinks (#72)#74
lifeizhou-ap merged 1 commit intomainfrom
jonandersen/inline-agents-file

Conversation

@jonandersen
Copy link
Collaborator

@jonandersen jonandersen commented Feb 17, 2026

Generate a single inlined AGENTS.md file with all rule content and symlink
agent output files to it, replacing @ file references that Codex and other
agents can't resolve.

Add description headers and spacing to inlined content

Use frontmatter description as a markdown heading before each rule's body
content, and separate rules with blank lines for better readability.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
Signed-off-by: Jon Andersen jon.andersen.se@gmail.com

Copy link
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@jonandersen jonandersen marked this pull request as ready for review February 17, 2026 20:56
@jonandersen jonandersen marked this pull request as draft February 17, 2026 21:00
Generate a single inlined file with all rule content and
symlink agent output files to it, replacing @ file references
that Codex and other agents can't resolve. Each rule is
preceded by a heading from its frontmatter description field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Jon Andersen <jon.andersen.se@gmail.com>
@jonandersen jonandersen force-pushed the jonandersen/inline-agents-file branch from 1b3d01d to 6ac036a Compare February 18, 2026 00:34
@jonandersen jonandersen marked this pull request as ready for review February 18, 2026 00:34
@jonandersen jonandersen requested a review from Copilot February 18, 2026 01:18
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the AI rules generation system to create a single inlined ai-rules-generated-AGENTS.md file containing all rule content, with most agent output files (CLAUDE.md, AGENTS.md, GEMINI.md, etc.) becoming symlinks to this inlined file. This change solves a critical problem where agents like Codex couldn't resolve @ file references that were previously used.

Changes:

  • Introduced a new inlined agents file that concatenates all rule content with description headers for better structure
  • Modified agent generators to create symlinks to the inlined file instead of files with @ references
  • Updated status checking logic to validate inlined symlinks
  • Added comprehensive test updates to use run_generate for more realistic test scenarios

Reviewed changes

Copilot reviewed 22 out of 25 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/constants.rs Added INLINED_AGENTS_FILENAME constant
src/operations/body_generator.rs Added functions to generate inlined content with description headers
src/operations/mod.rs Updated exports to include new inlined content generation function
src/utils/file_utils.rs Added symlink creation and checking functions for inlined file
src/agents/rule_generator.rs Added trait methods for inlined symlink support
src/agents/single_file_based.rs Implemented inlined symlink methods for single-file agents
src/agents/claude.rs Updated to use inlined symlinks in non-skills mode
src/agents/gemini.rs Implemented inlined symlink support
src/agents/codex.rs Implemented inlined symlink support
src/agents/amp.rs Implemented inlined symlink support
src/agents/roo.rs Delegated inlined symlink methods to inner generator
src/commands/generate.rs Refactored to generate inlined file and create symlinks for compatible agents
src/commands/status.rs Updated to check inlined symlinks and refactored tests to use run_generate
src/commands/mod.rs Updated integration test to handle symlink behavior
docs/rule-format.md Documented how standard mode works with inlined files
docs/project-structure.md Updated structure documentation to show symlinks
docs/agents.md Updated agent support matrix to indicate symlink usage
ai-rules/.generated-ai-rules/ai-rules-generated-AGENTS.md Generated inlined file with all rule content
CLAUDE.md, AGENTS.md Changed from content files to symlinks
Example/ai-rules/required-secret-file.md Added test file for examples
Example/ai-rules/example-required.md Minor grammar fix
Cargo.lock Version bump to 1.4.0

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} else {
generate_all_rule_references(source_files)
};
// In skills mode: check inlined required content
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "In skills mode: check inlined required content" is misleading. This code path executes in both skills and non-skills modes when source files exist (it's in the else branch at line 87). However, since check_agent_contents is only called when uses_inlined_symlink returns false (which only happens in skills mode), the comment is technically correct but confusing. Consider clarifying the comment to something like "Check inlined required content (note: this method is only called in skills mode)".

Suggested change
// In skills mode: check inlined required content
// Check inlined required content (note: this method is only called in skills mode)

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +114
pub fn generate_inlined_required_content(source_files: &[SourceFile]) -> String {
let mut parts: Vec<String> = Vec::new();

for source_file in source_files {
if source_file.front_matter.always_apply {
let mut part = String::new();
if !source_file.front_matter.description.is_empty() {
part.push_str(&format!("# {}\n\n", source_file.front_matter.description));
}
part.push_str(&ensure_trailing_newline(source_file.body.clone()));
parts.push(part);
}
}

parts.join("\n")
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding unit tests for the new functions generate_inlined_required_content and generate_inlined_agents_content similar to the existing tests for generate_required_rule_references and generate_all_rule_references. This would improve test coverage and make it easier to verify edge cases like empty descriptions, multiple rules with varying descriptions, and the formatting of the inlined content.

Copilot uses AI. Check for mistakes.

Ok(resolved_canonical == expected_canonical && expected_target.exists())
}

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding unit tests for the new functions create_symlink_to_inlined_file and check_inlined_file_symlink, similar to the existing tests for create_symlink_to_agents_md and check_agents_md_symlink. This would provide better test coverage for edge cases like missing source files, wrong symlink targets, and relative path calculations for the inlined file.

Suggested change
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn unique_temp_dir(prefix: &str) -> PathBuf {
let base = env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let dir = base.join(format!("{prefix}_{nanos}"));
// Best-effort cleanup if it already exists
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("failed to create temp dir");
dir
}
#[test]
fn create_symlink_to_inlined_file_returns_false_when_source_missing() {
let temp_dir = unique_temp_dir("inlined_missing_source");
let output_path = Path::new("inlined_link.md");
let created = create_symlink_to_inlined_file(&temp_dir, output_path).unwrap();
assert!(!created, "expected create_symlink_to_inlined_file to return false when source is missing");
let link_path = temp_dir.join(output_path);
assert!(
!link_path.exists(),
"symlink should not be created when source file does not exist"
);
}
#[test]
fn create_and_check_inlined_file_symlink_success() {
let temp_dir = unique_temp_dir("inlined_symlink_success");
// Determine where the inlined file should live relative to current_dir
let inlined_relative = inlined_agents_relative_path();
let source_full_path = temp_dir.join(&inlined_relative);
if let Some(parent) = source_full_path.parent() {
fs::create_dir_all(parent).expect("failed to create parent dirs for inlined file");
}
fs::write(&source_full_path, b"test inlined content")
.expect("failed to write inlined source file");
let output_path = Path::new("inlined_link.md");
let created = create_symlink_to_inlined_file(&temp_dir, output_path).unwrap();
assert!(created, "expected create_symlink_to_inlined_file to return true when source exists");
let link_path = temp_dir.join(output_path);
assert!(
link_path.is_symlink(),
"expected output path to be a symlink"
);
let is_valid = check_inlined_file_symlink(&temp_dir, &link_path).unwrap();
assert!(
is_valid,
"check_inlined_file_symlink should return true for a correctly created symlink"
);
}
#[test]
fn check_inlined_file_symlink_returns_false_for_wrong_target() {
let temp_dir = unique_temp_dir("inlined_wrong_target");
// Create the expected inlined source file
let inlined_relative = inlined_agents_relative_path();
let expected_source_full = temp_dir.join(&inlined_relative);
if let Some(parent) = expected_source_full.parent() {
fs::create_dir_all(parent).expect("failed to create parent dirs for inlined file");
}
fs::write(&expected_source_full, b"correct inlined content")
.expect("failed to write expected inlined source file");
// Create a different file and point the symlink there
let other_target = temp_dir.join("other_target.txt");
fs::write(&other_target, b"other content").expect("failed to write other target file");
let symlink_path = temp_dir.join("wrong_inlined_link.md");
unix_fs::symlink(&other_target, &symlink_path)
.expect("failed to create symlink to wrong target");
let is_valid = check_inlined_file_symlink(&temp_dir, &symlink_path).unwrap();
assert!(
!is_valid,
"check_inlined_file_symlink should return false when symlink points to the wrong target"
);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +122
pub fn create_symlink_to_inlined_file(current_dir: &Path, output_path: &Path) -> Result<bool> {
let inlined_relative = inlined_agents_relative_path();
let source_full_path = current_dir.join(&inlined_relative);

if !source_full_path.exists() {
return Ok(false);
}

let link = current_dir.join(output_path);
let relative_source = calculate_relative_path(output_path, &inlined_relative);

create_relative_symlink(&link, &relative_source)?;

Ok(true)
}

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The functions create_symlink_to_inlined_file and create_symlink_to_agents_md have very similar implementations. Consider refactoring them to use a shared helper function that takes the source path as a parameter, to reduce code duplication and improve maintainability. For example: create_symlink_to_file(current_dir: &Path, output_path: &Path, source_relative: PathBuf) -> Result<bool>.

Suggested change
pub fn create_symlink_to_inlined_file(current_dir: &Path, output_path: &Path) -> Result<bool> {
let inlined_relative = inlined_agents_relative_path();
let source_full_path = current_dir.join(&inlined_relative);
if !source_full_path.exists() {
return Ok(false);
}
let link = current_dir.join(output_path);
let relative_source = calculate_relative_path(output_path, &inlined_relative);
create_relative_symlink(&link, &relative_source)?;
Ok(true)
}
fn create_symlink_to_file(
current_dir: &Path,
output_path: &Path,
source_relative: PathBuf,
) -> Result<bool> {
let source_full_path = current_dir.join(&source_relative);
if !source_full_path.exists() {
return Ok(false);
}
let link = current_dir.join(output_path);
let relative_source = calculate_relative_path(output_path, &source_relative);
create_relative_symlink(&link, &relative_source)?;
Ok(true)
}
pub fn create_symlink_to_inlined_file(current_dir: &Path, output_path: &Path) -> Result<bool> {
let inlined_relative = inlined_agents_relative_path();
create_symlink_to_file(current_dir, output_path, inlined_relative)
}

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +145
pub fn check_agents_md_symlink(current_dir: &Path, symlink_path: &Path) -> Result<bool> {
if !symlink_path.is_symlink() {
return Ok(false);
}

let expected_target = current_dir
.join(AI_RULE_SOURCE_DIR)
.join(AGENTS_MD_FILENAME);
let actual_target = fs::read_link(symlink_path)?;

let resolved_target = if actual_target.is_absolute() {
actual_target
} else {
// For relative paths, resolve from the symlink's parent directory
let symlink_parent = symlink_path.parent().unwrap_or(current_dir);
symlink_parent.join(&actual_target)
};

// Canonicalize both paths to handle ".." components properly
let resolved_canonical = resolved_target.canonicalize().unwrap_or(resolved_target);
let expected_canonical = expected_target
.canonicalize()
.unwrap_or_else(|_| expected_target.clone());

Ok(resolved_canonical == expected_canonical && expected_target.exists())
}

pub fn create_symlink_to_inlined_file(current_dir: &Path, output_path: &Path) -> Result<bool> {
let inlined_relative = inlined_agents_relative_path();
let source_full_path = current_dir.join(&inlined_relative);

if !source_full_path.exists() {
return Ok(false);
}

let link = current_dir.join(output_path);
let relative_source = calculate_relative_path(output_path, &inlined_relative);

create_relative_symlink(&link, &relative_source)?;

Ok(true)
}

pub fn check_inlined_file_symlink(current_dir: &Path, symlink_path: &Path) -> Result<bool> {
if !symlink_path.is_symlink() {
return Ok(false);
}

let inlined_relative = inlined_agents_relative_path();
let expected_target = current_dir.join(&inlined_relative);
let actual_target = fs::read_link(symlink_path)?;

let resolved_target = if actual_target.is_absolute() {
actual_target
} else {
let symlink_parent = symlink_path.parent().unwrap_or(current_dir);
symlink_parent.join(&actual_target)
};

let resolved_canonical = resolved_target.canonicalize().unwrap_or(resolved_target);
let expected_canonical = expected_target
.canonicalize()
.unwrap_or_else(|_| expected_target.clone());

Ok(resolved_canonical == expected_canonical && expected_target.exists())
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The functions check_inlined_file_symlink and check_agents_md_symlink have identical implementations except for the expected target path. Consider refactoring them to use a shared helper function that takes the expected target path as a parameter, to reduce code duplication and improve maintainability. For example: check_symlink_target(current_dir: &Path, symlink_path: &Path, expected_relative: PathBuf) -> Result<bool>.

Copilot uses AI. Check for mistakes.
@lifeizhou-ap lifeizhou-ap merged commit 78e87fb into main Feb 19, 2026
14 checks passed
@lifeizhou-ap lifeizhou-ap deleted the jonandersen/inline-agents-file branch February 19, 2026 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants