#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Component, Path, PathBuf};
use chrono::Utc;
use crate::core::primer::{
PrimerFormat, PrimerReport, PrimerSection, primer_settings_from_workspace,
run_primer_with_persistence,
};
use crate::curate::{CandidateSource, CandidateStatus, CandidateType};
use crate::db::{
CreateAuditInput, CreateCurationCandidateInput, CreateEvidenceSpanInput, CreateSessionInput,
DbConnection, EvidenceProducerKind, StoredMemory, audit_actions, generate_audit_id,
};
use crate::models::{CandidateId, DomainError};
use crate::search::HashEmbedder;
use crate::search::simhash::{cosine_similarity, hamming_distance, simhash_128};
pub const AGENTSMD_EXPORT_SCHEMA_V1: &str = "ee.agentsmd.export.v1";
pub const AGENTSMD_IMPORT_SCHEMA_V1: &str = "ee.agentsmd.import.v1";
pub const AGENTSMD_DRIFT_SCHEMA_V1: &str = "ee.agentsmd.drift.v1";
pub const AGENTSMD_FILE_MISSING_CODE: &str = "agentsmd_file_missing";
pub const AGENTSMD_MARKERS_MISSING_CODE: &str = "agentsmd_markers_missing";
pub const AGENTSMD_UNMANAGED_EDIT_DETECTED_CODE: &str = "agentsmd_unmanaged_edit_detected";
const AGENTSMD_IMPORT_AUDIT_SCHEMA_V1: &str = "ee.audit.agentsmd_import.v1";
const AGENTSMD_IMPORT_SESSION_KEY: &str = "ee-agentsmd-import";
const AGENTSMD_IMPORT_EVIDENCE_SCHEMA_V1: &str = "ee.agentsmd.import_evidence.v1";
pub const AGENTSMD_DEFAULT_FILE: &str = "AGENTS.md";
pub const AGENTSMD_BACKUP_SUFFIX: &str = ".ee-backup";
const MARKER_BEGIN_PREFIX: &str = "<!-- ee:agentsmd:begin";
const MARKER_END: &str = "<!-- ee:agentsmd:end -->";
const AGENTSMD_DEDUP_SCAN_LIMIT: usize = 256;
const AGENTSMD_DEDUP_HAMMING_K: u32 = 32;
const AGENTSMD_DEDUP_CANDIDATE_LIMIT: usize = 16;
const AGENTSMD_RULE_MIN_CHARS: usize = 20;
const AGENTSMD_RULE_MAX_CHARS: usize = 400;
const AGENTSMD_CONTRADICTION_SIMILARITY: f32 = 0.55;
const AGENTSMD_CONTRADICTION_MIN_CONFIDENCE: f32 = 0.7;
const AGENTSMD_IMPORT_CONFIDENCE: f32 = 0.5;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentsmdDegradation {
pub code: &'static str,
pub severity: &'static str,
pub message: String,
pub repair: Option<String>,
}
impl AgentsmdDegradation {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"code": self.code,
"severity": self.severity,
"message": self.message,
"repair": self.repair,
})
}
}
fn file_missing_degradation(display_path: &str, create_hint: bool) -> AgentsmdDegradation {
let repair = if create_hint {
Some(format!(
"ee export agentsmd --workspace . --file {display_path} --create"
))
} else {
None
};
AgentsmdDegradation {
code: AGENTSMD_FILE_MISSING_CODE,
severity: "info",
message: format!(
"Bridge target {display_path} does not exist; nothing to read. Pass --create to \
materialize it with a fresh managed block."
),
repair,
}
}
fn markers_missing_degradation(display_path: &str) -> AgentsmdDegradation {
AgentsmdDegradation {
code: AGENTSMD_MARKERS_MISSING_CODE,
severity: "info",
message: format!(
"{display_path} has no ee:agentsmd managed block yet (import-only file or first \
export); the bridge only ever writes between its own markers."
),
repair: None,
}
}
fn unmanaged_edit_degradation(display_path: &str) -> AgentsmdDegradation {
AgentsmdDegradation {
code: AGENTSMD_UNMANAGED_EDIT_DETECTED_CODE,
severity: "warning",
message: format!(
"The managed block in {display_path} was hand-edited since the last export \
(content hash mismatch); export refuses to overwrite it without \
--force-managed-block. The hand edit is preserved in {display_path}{AGENTSMD_BACKUP_SUFFIX} \
when forced."
),
repair: Some(
"ee export agentsmd --workspace . --dry-run # review, then --force-managed-block"
.to_owned(),
),
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManagedBlock {
pub begin_index: usize,
pub end_index: usize,
pub generation: Option<i64>,
pub recorded_hash: Option<String>,
pub body: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ManagedBlockScan {
Missing,
Found(ManagedBlock),
}
fn is_marker_begin_line(line: &str) -> bool {
let Some(rest) = line.strip_prefix(MARKER_BEGIN_PREFIX) else {
return false;
};
rest.chars().next().is_some_and(char::is_whitespace) || rest == "-->"
}
pub fn scan_managed_block(content: &str) -> Result<ManagedBlockScan, String> {
let mut begin: Option<(usize, Option<i64>, Option<String>)> = None;
let mut found: Option<ManagedBlock> = None;
let lines: Vec<&str> = content.lines().collect();
for (index, raw_line) in lines.iter().enumerate() {
let line = raw_line.trim();
if is_marker_begin_line(line) {
if begin.is_some() {
return Err(format!(
"nested ee:agentsmd begin marker at line {}",
index + 1
));
}
if found.is_some() {
return Err(format!(
"second ee:agentsmd managed block at line {}",
index + 1
));
}
if !line.ends_with("-->") {
return Err(format!(
"unterminated ee:agentsmd begin marker at line {}",
index + 1
));
}
let attributes = line
.strip_prefix(MARKER_BEGIN_PREFIX)
.unwrap_or_default()
.trim_end_matches("-->")
.trim();
let mut generation = None;
let mut recorded_hash = None;
for attribute in attributes.split_whitespace() {
if let Some(value) = attribute.strip_prefix("generation=") {
generation = value.parse::<i64>().ok();
} else if let Some(value) = attribute.strip_prefix("hash=") {
recorded_hash = Some(value.to_owned());
}
}
begin = Some((index, generation, recorded_hash));
} else if line == MARKER_END {
let Some((begin_index, generation, recorded_hash)) = begin.take() else {
return Err(format!(
"ee:agentsmd end marker without begin at line {}",
index + 1
));
};
let body = lines[begin_index + 1..index].join("\n");
found = Some(ManagedBlock {
begin_index,
end_index: index,
generation,
recorded_hash,
body,
});
}
}
if let Some((begin_index, _, _)) = begin {
return Err(format!(
"ee:agentsmd begin marker at line {} has no end marker",
begin_index + 1
));
}
Ok(found.map_or(ManagedBlockScan::Missing, ManagedBlockScan::Found))
}
#[must_use]
pub fn managed_block_body_hash(body: &str) -> String {
let canonical = body.strip_suffix('\n').unwrap_or(body);
format!(
"blake3:{}",
blake3::hash(canonical.as_bytes())
.to_hex()
.chars()
.take(16)
.collect::<String>()
)
}
#[must_use]
pub fn render_managed_body(sections: &[PrimerSection]) -> String {
let mut body = String::new();
body.push_str(
"<!-- generated by `ee export agentsmd`; hand-edit OUTSIDE the ee markers only -->\n",
);
for section in sections {
let heading = match section.name.as_str() {
"rules" => "## Workspace rules (ee memory)",
"warnings" => "## Workspace warnings (ee memory)",
_ => continue,
};
if section.items.is_empty() {
continue;
}
body.push('\n');
body.push_str(heading);
body.push('\n');
body.push('\n');
for item in §ion.items {
body.push_str("- ");
body.push_str(&item.line);
body.push('\n');
}
}
body
}
#[must_use]
pub fn render_managed_block(body: &str, db_generation: i64) -> String {
format!(
"{MARKER_BEGIN_PREFIX} generation={db_generation} hash={} -->\n{body}{MARKER_END}",
managed_block_body_hash(body),
)
}
#[must_use]
pub fn render_block_diff(old_block: &str, new_block: &str) -> String {
let mut diff = String::new();
for line in old_block.lines() {
diff.push_str("- ");
diff.push_str(line);
diff.push('\n');
}
for line in new_block.lines() {
diff.push_str("+ ");
diff.push_str(line);
diff.push('\n');
}
diff
}
fn malformed_markers_error(display_path: &str, reason: &str) -> DomainError {
DomainError::Usage {
message: format!(
"Refusing to touch {display_path}: malformed ee:agentsmd markers ({reason}). The \
bridge only operates on a single well-formed begin/end marker pair."
),
repair: Some(format!(
"Repair or remove the ee:agentsmd marker lines in {display_path}, then re-run."
)),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RulePolarity {
Positive,
Negative,
}
impl RulePolarity {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Positive => "positive",
Self::Negative => "negative",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedStatement {
pub line_number: usize,
pub text: String,
pub kind: &'static str,
pub polarity: RulePolarity,
pub modality: &'static str,
}
fn strip_bullet_prefix(line: &str) -> Option<&str> {
let trimmed = line.trim_start();
for prefix in ["- ", "* ", "+ "] {
if let Some(rest) = trimmed.strip_prefix(prefix) {
return Some(rest);
}
}
let mut chars = trimmed.char_indices();
let mut digits = 0_usize;
for (index, character) in chars.by_ref() {
if character.is_ascii_digit() {
digits = index + 1;
} else {
break;
}
}
if digits > 0 {
if let Some(rest) = trimmed.get(digits..) {
if let Some(rest) = rest.strip_prefix(". ") {
return Some(rest);
}
}
}
None
}
fn uppercase_modality(text: &str) -> Option<(&'static str, RulePolarity, &'static str)> {
let tokens: Vec<&str> = text
.split_whitespace()
.map(|token| token.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'))
.collect();
for (index, token) in tokens.iter().enumerate() {
let next = tokens.get(index + 1).copied().unwrap_or_default();
match *token {
"MUST" if next == "NOT" => return Some(("rule", RulePolarity::Negative, "MUST NOT")),
"MUST" => return Some(("rule", RulePolarity::Positive, "MUST")),
"NEVER" => return Some(("rule", RulePolarity::Negative, "NEVER")),
"ALWAYS" => return Some(("rule", RulePolarity::Positive, "ALWAYS")),
"DO" if next == "NOT" => return Some(("rule", RulePolarity::Negative, "DO NOT")),
"DON'T" | "DON’T" => return Some(("rule", RulePolarity::Negative, "DON'T")),
_ => {}
}
}
None
}
fn leading_cue_modality(text: &str) -> Option<(&'static str, RulePolarity, &'static str)> {
const CUES: &[(&str, &str, RulePolarity, &str)] = &[
("Never ", "rule", RulePolarity::Negative, "Never"),
("Always ", "rule", RulePolarity::Positive, "Always"),
("Do not ", "rule", RulePolarity::Negative, "Do not"),
("Don't ", "rule", RulePolarity::Negative, "Don't"),
("Don’t ", "rule", RulePolarity::Negative, "Don't"),
("Avoid ", "convention", RulePolarity::Negative, "Avoid"),
("Prefer ", "convention", RulePolarity::Positive, "Prefer"),
];
for (cue, kind, polarity, modality) in CUES {
if text.starts_with(cue) {
return Some((kind, *polarity, modality));
}
}
None
}
#[must_use]
pub fn classify_statement(
text: &str,
from_bullet: bool,
) -> Option<(&'static str, RulePolarity, &'static str)> {
let length = text.chars().count();
if !(AGENTSMD_RULE_MIN_CHARS..=AGENTSMD_RULE_MAX_CHARS).contains(&length) {
return None;
}
if let Some(classified) = uppercase_modality(text) {
return Some(classified);
}
if from_bullet {
return leading_cue_modality(text);
}
None
}
#[must_use]
pub fn parse_rule_statements(
content: &str,
exclude: Option<(usize, usize)>,
) -> Vec<ParsedStatement> {
let mut statements = Vec::new();
let mut in_fence = false;
for (index, raw_line) in content.lines().enumerate() {
if let Some((begin, end)) = exclude {
if index >= begin && index <= end {
continue;
}
}
let trimmed = raw_line.trim();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
in_fence = !in_fence;
continue;
}
if in_fence
|| trimmed.is_empty()
|| trimmed.starts_with('#')
|| trimmed.starts_with("<!--")
|| trimmed.starts_with('|')
|| trimmed.starts_with('>')
{
continue;
}
let (text, from_bullet) = strip_bullet_prefix(raw_line)
.map_or((trimmed, false), |stripped| (stripped.trim(), true));
let text = text
.trim_start_matches("**")
.trim_end_matches("**")
.trim()
.to_owned();
let Some((kind, polarity, modality)) = classify_statement(&text, from_bullet) else {
continue;
};
statements.push(ParsedStatement {
line_number: index + 1,
text,
kind,
polarity,
modality,
});
}
statements
}
fn top_neighbor(memories: &[StoredMemory], content: &str) -> Option<(String, f32)> {
if memories.is_empty() {
return None;
}
let window_start = memories.len().saturating_sub(AGENTSMD_DEDUP_SCAN_LIMIT);
let query_fingerprint = simhash_128(content);
let mut gated: Vec<(u32, &StoredMemory)> = memories[window_start..]
.iter()
.filter_map(|memory| {
let distance = hamming_distance(query_fingerprint, simhash_128(&memory.content));
(distance <= AGENTSMD_DEDUP_HAMMING_K).then_some((distance, memory))
})
.collect();
gated.sort_by(|(left_distance, left), (right_distance, right)| {
left_distance
.cmp(right_distance)
.then_with(|| left.id.cmp(&right.id))
});
gated.truncate(AGENTSMD_DEDUP_CANDIDATE_LIMIT);
let embedder = HashEmbedder::default_256();
let query_embedding = embedder.embed_sync(content);
let mut top: Option<(String, f32, u32)> = None;
for (hamming, memory) in gated {
let candidate_embedding = embedder.embed_sync(&memory.content);
let Some(similarity) = cosine_similarity(&query_embedding, &candidate_embedding) else {
continue;
};
let better = match &top {
None => true,
Some((current_id, current_similarity, current_hamming)) => {
match similarity.partial_cmp(current_similarity) {
Some(std::cmp::Ordering::Greater) => true,
Some(std::cmp::Ordering::Equal) => {
(hamming, memory.id.as_str()) < (*current_hamming, current_id.as_str())
}
_ => false,
}
}
};
if better {
top = Some((memory.id.clone(), similarity, hamming));
}
}
top.map(|(memory_id, similarity, _)| (memory_id, similarity))
}
fn duplicate_similarity_threshold(workspace_path: &Path) -> f32 {
crate::config::workspace_config(workspace_path)
.and_then(|config| config.curation.duplicate_similarity)
.map_or(
crate::core::memory::REMEMBER_DEFAULT_DUPLICATE_SIMILARITY,
|value| value as f32,
)
}
fn deterministic_agentsmd_id(prefix: &str, parts: &[&str]) -> String {
let mut hasher = blake3::Hasher::new();
for part in parts {
hasher.update(part.as_bytes());
hasher.update(b"\0");
}
let hash = hasher.finalize();
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&hash.as_bytes()[..16]);
let candidate = CandidateId::from_uuid(uuid::Uuid::from_bytes(bytes)).to_string();
format!("{prefix}{}", candidate.trim_start_matches("cand_"))
}
fn storage_error(context: &str, error: impl std::fmt::Display) -> DomainError {
DomainError::Storage {
message: format!("{context}: {error}"),
repair: Some("ee doctor".to_owned()),
}
}
#[derive(Clone, Debug, Default)]
pub struct AgentsmdExportOptions {
pub file: Option<PathBuf>,
pub tokens: Option<u32>,
pub dry_run: bool,
pub create: bool,
pub force_managed_block: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentsmdExportReport {
pub status: &'static str,
pub workspace_id: String,
pub file: String,
pub db_generation: i64,
pub dry_run: bool,
pub created: bool,
pub changed: bool,
pub backup_path: Option<String>,
pub block_hash: String,
pub rules_count: usize,
pub warnings_count: usize,
pub redaction_skipped: u32,
pub diff: Option<String>,
pub degraded: Vec<AgentsmdDegradation>,
}
impl AgentsmdExportReport {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"schema": AGENTSMD_EXPORT_SCHEMA_V1,
"command": "export agentsmd",
"status": self.status,
"workspaceId": self.workspace_id,
"file": self.file,
"dbGeneration": self.db_generation,
"dryRun": self.dry_run,
"created": self.created,
"changed": self.changed,
"backupPath": self.backup_path,
"blockHash": self.block_hash,
"rulesCount": self.rules_count,
"warningsCount": self.warnings_count,
"redactionSkipped": self.redaction_skipped,
"diff": self.diff,
"degraded": self.degraded.iter().map(AgentsmdDegradation::data_json).collect::<Vec<_>>(),
})
}
#[must_use]
pub fn human_summary(&self) -> String {
let mut out = format!(
"agentsmd export — {} (generation {})\nstatus: {}{}{}\nrules: {}, warnings: {}, redaction skipped: {}\n",
self.file,
self.db_generation,
self.status,
if self.dry_run { ", dry run" } else { "" },
if self.created {
", created"
} else if self.changed {
", changed"
} else {
", unchanged"
},
self.rules_count,
self.warnings_count,
self.redaction_skipped,
);
if let Some(backup) = &self.backup_path {
out.push_str(&format!("backup: {backup}\n"));
}
for entry in &self.degraded {
out.push_str(&format!("degraded: {} ({})\n", entry.code, entry.severity));
}
if let Some(diff) = &self.diff {
out.push_str("--- managed block diff ---\n");
out.push_str(diff);
}
out
}
}
fn invalid_bridge_file_path(path: &Path, reason: &str) -> DomainError {
DomainError::Usage {
message: format!(
"Invalid agentsmd bridge file path {}: {reason}. The path must stay inside the \
selected workspace.",
path.display()
),
repair: Some(
"Pass a workspace-relative file path such as AGENTS.md or CLAUDE.md.".to_owned(),
),
}
}
fn bridge_file_relative_path(path: &Path) -> Result<PathBuf, DomainError> {
if path.as_os_str().is_empty() {
return Err(invalid_bridge_file_path(path, "path is empty"));
}
if path.is_absolute() {
return Err(invalid_bridge_file_path(
path,
"absolute paths are not allowed",
));
}
let raw_path = path.to_string_lossy();
if raw_path.contains('\\') {
return Err(invalid_bridge_file_path(
path,
"backslash paths are not portable",
));
}
if raw_path
.split('/')
.any(|segment| matches!(segment, "." | ".."))
{
return Err(invalid_bridge_file_path(
path,
"dot path segments are not allowed",
));
}
let mut relative = PathBuf::new();
for component in path.components() {
match component {
Component::Normal(part) => relative.push(part),
Component::Prefix(_)
| Component::RootDir
| Component::CurDir
| Component::ParentDir => {
return Err(invalid_bridge_file_path(
path,
"only normal relative path components are allowed",
));
}
}
}
if relative.as_os_str().is_empty() {
return Err(invalid_bridge_file_path(path, "path is empty"));
}
Ok(relative)
}
fn resolve_bridge_file(
workspace_path: &Path,
file: Option<&Path>,
) -> Result<(PathBuf, String), DomainError> {
let requested = file.unwrap_or_else(|| Path::new(AGENTSMD_DEFAULT_FILE));
let relative = bridge_file_relative_path(requested)?;
let absolute = workspace_path.join(&relative);
let display = relative.display().to_string();
Ok((absolute, display))
}
fn read_bridge_file(path: &Path, display_path: &str) -> Result<Option<String>, DomainError> {
reject_bridge_symlink_component(path, display_path)?;
let mut file = match open_bridge_file_for_read(path) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(storage_error(
&format!("Failed to read {display_path}"),
error,
));
}
};
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|error| storage_error(&format!("Failed to read {display_path}"), error))?;
Ok(Some(content))
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn open_bridge_file_for_read(path: &Path) -> io::Result<File> {
open_bridge_leaf_no_follow(
path,
rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::from_raw_mode(0),
)
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn open_bridge_file_for_read(path: &Path) -> io::Result<File> {
std::fs::OpenOptions::new().read(true).open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn open_bridge_file_for_write(path: &Path) -> io::Result<File> {
open_bridge_leaf_no_follow(
path,
rustix::fs::OFlags::WRONLY
| rustix::fs::OFlags::CREATE
| rustix::fs::OFlags::TRUNC
| rustix::fs::OFlags::CLOEXEC,
rustix::fs::Mode::from_raw_mode(0o666),
)
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn open_bridge_file_for_write(path: &Path) -> io::Result<File> {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn open_bridge_leaf_no_follow(
path: &Path,
flags: rustix::fs::OFlags,
create_mode: rustix::fs::Mode,
) -> io::Result<File> {
let (parent, leaf) = open_bridge_parent_directory_no_follow(path)?;
let fd = rustix::fs::openat(
&parent,
leaf.as_os_str(),
flags | rustix::fs::OFlags::NOFOLLOW,
create_mode,
)
.map_err(io::Error::from)?;
Ok(File::from(fd))
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn open_bridge_parent_directory_no_follow(path: &Path) -> io::Result<(File, OsString)> {
let leaf = path.file_name().map(OsString::from).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"agentsmd bridge path {} has no final component",
path.display()
),
)
})?;
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
Ok((open_bridge_directory_chain_no_follow(parent)?, leaf))
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn open_bridge_directory_chain_no_follow(path: &Path) -> io::Result<File> {
let directory_flags =
rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::DIRECTORY | rustix::fs::OFlags::NOFOLLOW;
let mut directory = if path.is_absolute() {
let fd = rustix::fs::openat(
rustix::fs::CWD,
Path::new("/"),
directory_flags,
rustix::fs::Mode::from_raw_mode(0),
)
.map_err(io::Error::from)?;
File::from(fd)
} else {
let fd = rustix::fs::openat(
rustix::fs::CWD,
Path::new("."),
directory_flags,
rustix::fs::Mode::from_raw_mode(0),
)
.map_err(io::Error::from)?;
File::from(fd)
};
for component in path.components() {
match component {
Component::RootDir | Component::CurDir => {}
Component::Normal(part) => {
let fd = rustix::fs::openat(
&directory,
part,
directory_flags,
rustix::fs::Mode::from_raw_mode(0),
)
.map_err(io::Error::from)?;
directory = File::from(fd);
}
Component::ParentDir | Component::Prefix(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"unsupported agentsmd bridge directory component in {}",
path.display()
),
));
}
}
}
Ok(directory)
}
fn write_bridge_file(path: &Path, content: &str, display_path: &str) -> Result<(), DomainError> {
if let Some(parent) = path.parent() {
reject_bridge_symlink_component(parent, display_path)?;
std::fs::create_dir_all(parent).map_err(|error| {
storage_error(&format!("Failed to create parent of {display_path}"), error)
})?;
reject_bridge_symlink_component(parent, display_path)?;
}
reject_bridge_symlink_component(path, display_path)?;
let mut file = open_bridge_file_for_write(path)
.map_err(|error| storage_error(&format!("Failed to write {display_path}"), error))?;
file.write_all(content.as_bytes())
.map_err(|error| storage_error(&format!("Failed to write {display_path}"), error))
}
fn reject_bridge_symlink_component(path: &Path, display_path: &str) -> Result<(), DomainError> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component.as_os_str());
match std::fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(invalid_bridge_file_path(
Path::new(display_path),
"path traverses an existing symlinked component",
));
}
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(storage_error(
&format!("Failed to inspect {display_path} for symlink components"),
error,
));
}
}
}
Ok(())
}
fn assemble_bridge_primer(
connection: &DbConnection,
workspace_id: &str,
workspace_path: &Path,
tokens: Option<u32>,
) -> Result<PrimerReport, DomainError> {
let settings = primer_settings_from_workspace(workspace_path, PrimerFormat::Markdown, tokens);
run_primer_with_persistence(connection, workspace_id, &settings, false, false)
.map_err(|error| storage_error("Failed to assemble primer for agentsmd export", error))
}
fn splice_managed_block(existing: &str, found: &ManagedBlock, block: &str) -> (String, String) {
let segments: Vec<&str> = existing.split_inclusive('\n').collect();
let prefix_len: usize = segments[..found.begin_index].iter().map(|s| s.len()).sum();
let managed_len: usize = segments[found.begin_index..=found.end_index]
.iter()
.map(|s| s.len())
.sum();
let prefix = &existing[..prefix_len];
let managed_region = &existing[prefix_len..prefix_len + managed_len];
let suffix = &existing[prefix_len + managed_len..];
let end_terminator = if managed_region.ends_with("\r\n") {
"\r\n"
} else if managed_region.ends_with('\n') {
"\n"
} else {
""
};
let mut content =
String::with_capacity(prefix.len() + block.len() + end_terminator.len() + suffix.len());
content.push_str(prefix);
content.push_str(block);
content.push_str(end_terminator);
content.push_str(suffix);
(managed_region.to_string(), content)
}
pub fn run_agentsmd_export(
connection: &DbConnection,
workspace_id: &str,
workspace_path: &Path,
options: &AgentsmdExportOptions,
) -> Result<AgentsmdExportReport, DomainError> {
let primer = assemble_bridge_primer(connection, workspace_id, workspace_path, options.tokens)?;
let body = render_managed_body(&primer.sections);
let block = render_managed_block(&body, primer.db_generation);
let block_hash = managed_block_body_hash(&body);
let section_count = |name: &str| {
primer
.sections
.iter()
.find(|section| section.name == name)
.map_or(0, |section| section.items.len())
};
let (path, display_path) = resolve_bridge_file(workspace_path, options.file.as_deref())?;
let mut report = AgentsmdExportReport {
status: "ok",
workspace_id: workspace_id.to_owned(),
file: display_path.clone(),
db_generation: primer.db_generation,
dry_run: options.dry_run,
created: false,
changed: false,
backup_path: None,
block_hash,
rules_count: section_count("rules"),
warnings_count: section_count("warnings"),
redaction_skipped: primer.meta.skipped.redaction,
diff: None,
degraded: Vec::new(),
};
let Some(existing) = read_bridge_file(&path, &display_path)? else {
if !options.create {
report.status = "file_missing";
report
.degraded
.push(file_missing_degradation(&display_path, true));
return Ok(report);
}
report.created = true;
report.changed = true;
if options.dry_run {
report.diff = Some(render_block_diff("", &block));
return Ok(report);
}
write_bridge_file(&path, &format!("{block}\n"), &display_path)?;
return Ok(report);
};
let scan = scan_managed_block(&existing)
.map_err(|reason| malformed_markers_error(&display_path, &reason))?;
let (old_block, new_content) = match &scan {
ManagedBlockScan::Missing => {
report
.degraded
.push(markers_missing_degradation(&display_path));
let mut content = existing.clone();
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
if !content.is_empty() {
content.push('\n');
}
content.push_str(&block);
content.push('\n');
(String::new(), content)
}
ManagedBlockScan::Found(found) => {
let actual_hash = managed_block_body_hash(&found.body);
let hand_edited = found.recorded_hash.as_deref() != Some(actual_hash.as_str());
if hand_edited && !options.force_managed_block {
report.status = "refused_unmanaged_edit";
report
.degraded
.push(unmanaged_edit_degradation(&display_path));
return Ok(report);
}
splice_managed_block(&existing, found, &block)
}
};
report.changed = new_content != existing;
if options.dry_run {
if report.changed {
report.diff = Some(render_block_diff(&old_block, &block));
}
return Ok(report);
}
if !report.changed {
return Ok(report);
}
let backup_path = PathBuf::from(format!("{}{AGENTSMD_BACKUP_SUFFIX}", path.display()));
let backup_display = format!("{display_path}{AGENTSMD_BACKUP_SUFFIX}");
write_bridge_file(&backup_path, &existing, &backup_display)?;
report.backup_path = Some(backup_display);
write_bridge_file(&path, &new_content, &display_path)?;
Ok(report)
}
#[derive(Clone, Debug, Default)]
pub struct AgentsmdImportOptions {
pub file: Option<PathBuf>,
pub apply: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentsmdImportProposal {
pub proposal_id: String,
pub action: &'static str,
pub target_memory_id: Option<String>,
pub kind: &'static str,
pub content_draft: String,
pub evidence: Vec<String>,
pub line_number: usize,
pub modality: &'static str,
pub dedup_nearest_memory_id: Option<String>,
pub dedup_similarity: Option<f32>,
}
impl AgentsmdImportProposal {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"proposalId": &self.proposal_id,
"action": self.action,
"targetMemoryId": &self.target_memory_id,
"level": "procedural",
"kind": self.kind,
"contentDraft": &self.content_draft,
"evidence": &self.evidence,
"lineNumber": self.line_number,
"modality": self.modality,
"dedup": {
"nearestMemoryId": &self.dedup_nearest_memory_id,
"similarity": &self.dedup_similarity,
},
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentsmdImportAbstention {
pub line_number: usize,
pub text: String,
pub reason: &'static str,
}
impl AgentsmdImportAbstention {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"lineNumber": self.line_number,
"text": &self.text,
"reason": self.reason,
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AgentsmdImportApplied {
pub candidate_ids: Vec<String>,
pub audit_ids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentsmdImportReport {
pub status: &'static str,
pub workspace_id: String,
pub file: String,
pub dry_run: bool,
pub scanned_lines: usize,
pub managed_block_excluded: bool,
pub proposals: Vec<AgentsmdImportProposal>,
pub abstentions: Vec<AgentsmdImportAbstention>,
pub applied: Option<AgentsmdImportApplied>,
pub degraded: Vec<AgentsmdDegradation>,
}
impl AgentsmdImportReport {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"schema": AGENTSMD_IMPORT_SCHEMA_V1,
"command": "import agentsmd",
"status": self.status,
"workspaceId": self.workspace_id,
"file": self.file,
"dryRun": self.dry_run,
"scannedLines": self.scanned_lines,
"managedBlockExcluded": self.managed_block_excluded,
"proposals": self.proposals.iter().map(AgentsmdImportProposal::data_json).collect::<Vec<_>>(),
"abstentions": self.abstentions.iter().map(AgentsmdImportAbstention::data_json).collect::<Vec<_>>(),
"applied": self.applied.as_ref().map(|applied| serde_json::json!({
"candidateIds": &applied.candidate_ids,
"auditIds": &applied.audit_ids,
})),
"degraded": self.degraded.iter().map(AgentsmdDegradation::data_json).collect::<Vec<_>>(),
})
}
#[must_use]
pub fn human_summary(&self) -> String {
let creates = self
.proposals
.iter()
.filter(|proposal| proposal.action == "create_candidate")
.count();
let mut out = format!(
"agentsmd import — {}{}\nstatus: {}, scanned {} lines{}\nproposals: {} (create {}, reinforce {}), abstentions: {}\n",
self.file,
if self.dry_run { " (dry run)" } else { "" },
self.status,
self.scanned_lines,
if self.managed_block_excluded {
", managed block excluded"
} else {
""
},
self.proposals.len(),
creates,
self.proposals.len() - creates,
self.abstentions.len(),
);
for proposal in &self.proposals {
out.push_str(&format!(
"- L{} {} {}: {}\n",
proposal.line_number, proposal.kind, proposal.action, proposal.content_draft
));
}
if let Some(applied) = &self.applied {
out.push_str(&format!(
"applied: {} candidates, {} audit rows\n",
applied.candidate_ids.len(),
applied.audit_ids.len()
));
}
for entry in &self.degraded {
out.push_str(&format!("degraded: {} ({})\n", entry.code, entry.severity));
}
out
}
}
fn import_candidate_id(
workspace_id: &str,
action: &str,
kind: &str,
display_path: &str,
text: &str,
) -> String {
deterministic_agentsmd_id(
"curate_",
&[
workspace_id,
"agentsmd_import_candidate",
action,
kind,
display_path,
text,
],
)
}
pub fn run_agentsmd_import(
connection: &DbConnection,
workspace_id: &str,
workspace_path: &Path,
options: &AgentsmdImportOptions,
) -> Result<AgentsmdImportReport, DomainError> {
let (path, display_path) = resolve_bridge_file(workspace_path, options.file.as_deref())?;
let mut report = AgentsmdImportReport {
status: "ok",
workspace_id: workspace_id.to_owned(),
file: display_path.clone(),
dry_run: !options.apply,
scanned_lines: 0,
managed_block_excluded: false,
proposals: Vec::new(),
abstentions: Vec::new(),
applied: None,
degraded: Vec::new(),
};
let Some(content) = read_bridge_file(&path, &display_path)? else {
report.status = "file_missing";
report
.degraded
.push(file_missing_degradation(&display_path, false));
return Ok(report);
};
report.scanned_lines = content.lines().count();
let exclude = match scan_managed_block(&content)
.map_err(|reason| malformed_markers_error(&display_path, &reason))?
{
ManagedBlockScan::Found(block) => {
report.managed_block_excluded = true;
Some((block.begin_index, block.end_index))
}
ManagedBlockScan::Missing => {
report
.degraded
.push(markers_missing_degradation(&display_path));
None
}
};
let statements = parse_rule_statements(&content, exclude);
if statements.is_empty() {
return Ok(report);
}
let duplicate_threshold = duplicate_similarity_threshold(workspace_path);
let memories = connection
.list_memories(workspace_id, None, false)
.map_err(|error| storage_error("Failed to list memories for agentsmd dedup", error))?;
for statement in statements {
let neighbor = top_neighbor(&memories, &statement.text);
let (action, target_memory_id): (&'static str, Option<String>) = match &neighbor {
Some((memory_id, similarity)) if *similarity >= duplicate_threshold => {
("reinforce_existing", Some(memory_id.clone()))
}
_ => ("create_candidate", None),
};
let candidate_id = import_candidate_id(
workspace_id,
action,
statement.kind,
&display_path,
&statement.text,
);
let already_present = connection
.get_curation_candidate(workspace_id, &candidate_id)
.map_err(|error| storage_error("Failed to check existing agentsmd candidate", error))?
.is_some();
if already_present {
report.abstentions.push(AgentsmdImportAbstention {
line_number: statement.line_number,
text: statement.text,
reason: "already_imported",
});
continue;
}
let proposal_id = deterministic_agentsmd_id(
"aip_",
&[
workspace_id,
"agentsmd_import",
&display_path,
statement.kind,
&statement.text,
],
);
report.proposals.push(AgentsmdImportProposal {
proposal_id,
action,
target_memory_id,
kind: statement.kind,
content_draft: statement.text,
evidence: vec![format!("file://{display_path}#L{}", statement.line_number)],
line_number: statement.line_number,
modality: statement.modality,
dedup_nearest_memory_id: neighbor.as_ref().map(|(memory_id, _)| memory_id.clone()),
dedup_similarity: neighbor.as_ref().map(|(_, similarity)| *similarity),
});
}
if options.apply {
report.applied = Some(apply_import_proposals(
connection,
workspace_id,
&display_path,
&report.proposals,
duplicate_threshold,
)?);
}
Ok(report)
}
fn ensure_agentsmd_session(
connection: &DbConnection,
workspace_id: &str,
) -> Result<String, DomainError> {
if let Some(session) = connection
.get_session_by_cass_id(workspace_id, AGENTSMD_IMPORT_SESSION_KEY)
.map_err(|error| storage_error("Failed to look up agentsmd import session", error))?
{
return Ok(session.id);
}
let session_id = {
let memory_id = crate::models::MemoryId::now().to_string();
let payload = memory_id.trim_start_matches("mem_").to_owned();
format!("sess_{payload}")
};
let input = CreateSessionInput {
workspace_id: workspace_id.to_owned(),
cass_session_id: AGENTSMD_IMPORT_SESSION_KEY.to_owned(),
source_path: None,
agent_name: None,
model: None,
started_at: None,
ended_at: None,
message_count: 0,
token_count: None,
content_hash: format!(
"blake3:{}",
blake3::hash(AGENTSMD_IMPORT_SESSION_KEY.as_bytes()).to_hex()
),
metadata_json: None,
};
match connection.insert_session(&session_id, &input) {
Ok(()) => Ok(session_id),
Err(error) => connection
.get_session_by_cass_id(workspace_id, AGENTSMD_IMPORT_SESSION_KEY)
.map_err(|query_error| {
storage_error(
"Failed to re-query raced agentsmd import session",
query_error,
)
})?
.map(|session| session.id)
.ok_or_else(|| storage_error("Failed to create agentsmd import session", error)),
}
}
fn apply_import_proposals(
connection: &DbConnection,
workspace_id: &str,
display_path: &str,
proposals: &[AgentsmdImportProposal],
duplicate_threshold: f32,
) -> Result<AgentsmdImportApplied, DomainError> {
let mut applied = AgentsmdImportApplied::default();
if proposals.is_empty() {
return Ok(applied);
}
let needs_session = proposals
.iter()
.any(|proposal| proposal.action == "create_candidate");
let session_id = if needs_session {
Some(ensure_agentsmd_session(connection, workspace_id)?)
} else {
None
};
let imported_at = Utc::now().to_rfc3339();
for proposal in proposals {
let screening = crate::policy::screen_external_text_for_ingestion(&proposal.content_draft);
let screened_content = screening.content;
let inherited_redaction_classes = screening.redacted_reasons;
let source_path_hash = format!("blake3:{}", blake3::hash(display_path.as_bytes()).to_hex());
let canonical_source_ref = format!(
"agentsmd://{}#L{}",
source_path_hash.trim_start_matches("blake3:"),
proposal.line_number
);
let candidate_id = import_candidate_id(
workspace_id,
proposal.action,
proposal.kind,
&source_path_hash,
&screened_content,
);
let span_id = deterministic_agentsmd_id(
"ev_",
&[
workspace_id,
"agentsmd_import",
&source_path_hash,
&screened_content,
],
);
let statement_hash = format!(
"blake3:{}",
blake3::hash(screened_content.as_bytes()).to_hex()
);
let candidate_input = if proposal.action == "reinforce_existing" {
CreateCurationCandidateInput {
workspace_id: workspace_id.to_owned(),
candidate_type: CandidateType::Promote.as_str().to_owned(),
target_memory_id: proposal.target_memory_id.clone(),
proposed_content: None,
proposed_confidence: proposal.dedup_similarity,
proposed_trust_class: None,
source_type: CandidateSource::AgentInference.as_str().to_owned(),
source_id: Some("agentsmd_import".to_owned()),
reason: format!(
"AGENTS.md bridge import: near-duplicate of {} at similarity {:.4} \
(threshold {:.4}); reinforce the existing memory instead of creating a \
new one. Evidence: {}",
proposal.target_memory_id.as_deref().unwrap_or("unknown"),
proposal.dedup_similarity.unwrap_or_default(),
duplicate_threshold,
canonical_source_ref,
),
confidence: AGENTSMD_IMPORT_CONFIDENCE,
status: Some(CandidateStatus::Pending.as_str().to_owned()),
created_at: Some(imported_at.clone()),
ttl_expires_at: None,
derivation_source_refs_json: None,
derivation_metadata_json: None,
}
} else {
let source_refs_json = serde_json::json!([{
"kind": "evidence_span",
"id": &span_id,
"contentHash": &statement_hash,
}])
.to_string();
let metadata_json = serde_json::json!({
"memorySpec": {
"level": "procedural",
"kind": proposal.kind,
"tags": ["agentsmd-import"],
"confidence": AGENTSMD_IMPORT_CONFIDENCE,
"utility": serde_json::Value::Null,
"importance": serde_json::Value::Null,
"validFrom": serde_json::Value::Null,
"validTo": serde_json::Value::Null,
},
"producer": {
"producer": "agentsmd_import",
"producerPayload": {
"proposalId": &proposal.proposal_id,
"evidenceSpanId": &span_id,
"sourceRef": &canonical_source_ref,
"sourcePathHash": &source_path_hash,
"lineNumber": proposal.line_number,
"modality": proposal.modality,
},
},
})
.to_string();
CreateCurationCandidateInput {
workspace_id: workspace_id.to_owned(),
candidate_type: CandidateType::CreateDerivedMemory.as_str().to_owned(),
target_memory_id: None,
proposed_content: Some(screened_content.clone()),
proposed_confidence: Some(AGENTSMD_IMPORT_CONFIDENCE),
proposed_trust_class: Some("agent_assertion".to_owned()),
source_type: CandidateSource::AgentInference.as_str().to_owned(),
source_id: Some("agentsmd_import".to_owned()),
reason: format!(
"AGENTS.md bridge import: {} statement extracted from {}. Evidence: {}",
proposal.kind, "AGENTS.md", canonical_source_ref,
),
confidence: AGENTSMD_IMPORT_CONFIDENCE,
status: Some(CandidateStatus::Pending.as_str().to_owned()),
created_at: Some(imported_at.clone()),
ttl_expires_at: None,
derivation_source_refs_json: Some(source_refs_json),
derivation_metadata_json: Some(metadata_json),
}
};
let audit_id = generate_audit_id();
let audit_details = serde_json::json!({
"schema": AGENTSMD_IMPORT_AUDIT_SCHEMA_V1,
"command": "ee import agentsmd --apply",
"proposalId": &proposal.proposal_id,
"action": proposal.action,
"candidateId": &candidate_id,
"level": "procedural",
"kind": proposal.kind,
"evidence": [&canonical_source_ref],
"dedup": {
"nearestMemoryId": &proposal.dedup_nearest_memory_id,
"similarity": &proposal.dedup_similarity,
"threshold": duplicate_threshold,
},
"importedAt": &imported_at,
})
.to_string();
let audit_input = CreateAuditInput {
workspace_id: Some(workspace_id.to_owned()),
actor: Some("ee import agentsmd".to_owned()),
action: audit_actions::AGENTSMD_IMPORT.to_owned(),
target_type: Some("curation_candidate".to_owned()),
target_id: Some(candidate_id.clone()),
details: Some(audit_details),
};
connection
.with_transaction(|| {
if proposal.action == "create_candidate" {
let Some(session_id) = session_id.as_deref() else {
return Err(crate::db::DbError::MalformedRow {
operation: crate::db::DbOperation::Execute,
message: "agentsmd import session missing for create proposal"
.to_owned(),
});
};
if connection.get_evidence_span(&span_id)?.is_none() {
let line = u32::try_from(proposal.line_number).unwrap_or(1);
let metadata_json = serde_json::json!({
"schema": AGENTSMD_IMPORT_EVIDENCE_SCHEMA_V1,
"command": "ee import agentsmd --apply",
"sourceRef": &canonical_source_ref,
"sourcePathHash": &source_path_hash,
"lineNumber": proposal.line_number,
"modality": proposal.modality,
})
.to_string();
connection.insert_evidence_span(
&span_id,
&CreateEvidenceSpanInput {
workspace_id: workspace_id.to_owned(),
session_id: session_id.to_owned(),
memory_id: None,
producer_kind: EvidenceProducerKind::AgentsmdImport,
cass_span_id: canonical_source_ref.clone(),
span_kind: "summary".to_owned(),
start_line: line,
end_line: line,
start_byte: None,
end_byte: None,
role: Some("agentsmd_import".to_owned()),
excerpt: screened_content.clone(),
content_hash: statement_hash.clone(),
metadata_json: Some(metadata_json),
inherited_redaction_classes: inherited_redaction_classes.clone(),
},
)?;
}
}
connection.insert_curation_candidate(&candidate_id, &candidate_input)?;
connection.insert_audit(&audit_id, &audit_input)
})
.map_err(|error| storage_error("Failed to apply agentsmd import proposal", error))?;
applied.candidate_ids.push(candidate_id);
applied.audit_ids.push(audit_id);
}
Ok(applied)
}
#[derive(Clone, Debug, Default)]
pub struct AgentsmdDriftOptions {
pub file: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentsmdManagedBlockStatus {
pub generation: Option<i64>,
pub stale: bool,
pub hash_matches: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentsmdContradictionFinding {
pub line_number: usize,
pub file_text: String,
pub file_polarity: &'static str,
pub memory_id: String,
pub memory_polarity: &'static str,
pub similarity: f32,
pub signal: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentsmdMissingRuleFinding {
pub memory_id: String,
pub line: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AgentsmdDriftReport {
pub status: &'static str,
pub workspace_id: String,
pub file: String,
pub db_generation: i64,
pub managed_block: Option<AgentsmdManagedBlockStatus>,
pub contradictions: Vec<AgentsmdContradictionFinding>,
pub missing_rules: Vec<AgentsmdMissingRuleFinding>,
pub suggested_commands: Vec<String>,
pub degraded: Vec<AgentsmdDegradation>,
}
impl AgentsmdDriftReport {
#[must_use]
pub fn data_json(&self) -> serde_json::Value {
serde_json::json!({
"schema": AGENTSMD_DRIFT_SCHEMA_V1,
"command": "diag agentsmd-drift",
"status": self.status,
"workspaceId": self.workspace_id,
"file": self.file,
"dbGeneration": self.db_generation,
"managedBlock": self.managed_block.as_ref().map(|block| serde_json::json!({
"generation": block.generation,
"stale": block.stale,
"hashMatches": block.hash_matches,
})),
"contradictions": self.contradictions.iter().map(|finding| serde_json::json!({
"lineNumber": finding.line_number,
"fileText": &finding.file_text,
"filePolarity": finding.file_polarity,
"memoryId": &finding.memory_id,
"memoryPolarity": finding.memory_polarity,
"similarity": finding.similarity,
"signal": finding.signal,
})).collect::<Vec<_>>(),
"missingRules": self.missing_rules.iter().map(|finding| serde_json::json!({
"memoryId": &finding.memory_id,
"line": &finding.line,
})).collect::<Vec<_>>(),
"suggestedCommands": &self.suggested_commands,
"degraded": self.degraded.iter().map(AgentsmdDegradation::data_json).collect::<Vec<_>>(),
})
}
#[must_use]
pub fn human_summary(&self) -> String {
let mut out = format!(
"agentsmd drift — {} (db generation {})\nstatus: {}\n",
self.file, self.db_generation, self.status,
);
match &self.managed_block {
Some(block) => out.push_str(&format!(
"managed block: generation {}, {}, hash {}\n",
block
.generation
.map_or_else(|| "unknown".to_owned(), |generation| generation.to_string()),
if block.stale { "stale" } else { "current" },
if block.hash_matches {
"ok"
} else {
"MISMATCH (hand-edited)"
},
)),
None => out.push_str("managed block: none\n"),
}
out.push_str(&format!(
"contradictions: {}, missing rules: {}\n",
self.contradictions.len(),
self.missing_rules.len()
));
for finding in &self.contradictions {
out.push_str(&format!(
"- L{} {} vs {} ({}): {}\n",
finding.line_number,
finding.file_polarity,
finding.memory_id,
finding.memory_polarity,
finding.file_text,
));
}
for finding in &self.missing_rules {
out.push_str(&format!(
"- missing: {} ({})\n",
finding.line, finding.memory_id
));
}
for command in &self.suggested_commands {
out.push_str(&format!("suggest: {command}\n"));
}
for entry in &self.degraded {
out.push_str(&format!("degraded: {} ({})\n", entry.code, entry.severity));
}
out
}
}
fn push_unique(commands: &mut Vec<String>, command: String) {
if !commands.contains(&command) {
commands.push(command);
}
}
pub fn run_agentsmd_drift(
connection: &DbConnection,
workspace_id: &str,
workspace_path: &Path,
options: &AgentsmdDriftOptions,
) -> Result<AgentsmdDriftReport, DomainError> {
let db_generation = i64::try_from(
connection
.get_workspace_generation(workspace_id)
.map_err(|error| storage_error("Failed to read workspace generation", error))?
.unwrap_or(0),
)
.unwrap_or(i64::MAX);
let (path, display_path) = resolve_bridge_file(workspace_path, options.file.as_deref())?;
let mut report = AgentsmdDriftReport {
status: "ok",
workspace_id: workspace_id.to_owned(),
file: display_path.clone(),
db_generation,
managed_block: None,
contradictions: Vec::new(),
missing_rules: Vec::new(),
suggested_commands: Vec::new(),
degraded: Vec::new(),
};
let Some(content) = read_bridge_file(&path, &display_path)? else {
report.status = "file_missing";
report
.degraded
.push(file_missing_degradation(&display_path, true));
push_unique(
&mut report.suggested_commands,
"ee export agentsmd --workspace . --create".to_owned(),
);
return Ok(report);
};
let scan = scan_managed_block(&content)
.map_err(|reason| malformed_markers_error(&display_path, &reason))?;
let exclude = match &scan {
ManagedBlockScan::Found(block) => {
let hash_matches = block.recorded_hash.as_deref()
== Some(managed_block_body_hash(&block.body).as_str());
let stale = block
.generation
.is_none_or(|generation| generation < db_generation);
if stale {
push_unique(
&mut report.suggested_commands,
"ee export agentsmd --workspace .".to_owned(),
);
}
if !hash_matches {
report
.degraded
.push(unmanaged_edit_degradation(&display_path));
push_unique(
&mut report.suggested_commands,
"ee export agentsmd --workspace . --dry-run".to_owned(),
);
}
report.managed_block = Some(AgentsmdManagedBlockStatus {
generation: block.generation,
stale,
hash_matches,
});
Some((block.begin_index, block.end_index))
}
ManagedBlockScan::Missing => {
report
.degraded
.push(markers_missing_degradation(&display_path));
push_unique(
&mut report.suggested_commands,
"ee export agentsmd --workspace .".to_owned(),
);
None
}
};
let memories = connection
.list_memories(workspace_id, None, false)
.map_err(|error| storage_error("Failed to list memories for drift detection", error))?;
let rule_memories: Vec<&StoredMemory> = memories
.iter()
.filter(|memory| {
memory.level == "procedural"
&& memory.kind == "rule"
&& memory.confidence >= AGENTSMD_CONTRADICTION_MIN_CONFIDENCE
&& memory.tombstoned_at.is_none()
})
.collect();
let hand_statements = parse_rule_statements(&content, exclude);
if !rule_memories.is_empty() && !hand_statements.is_empty() {
let embedder = HashEmbedder::default_256();
for statement in &hand_statements {
let statement_embedding = embedder.embed_sync(&statement.text);
let mut best: Option<(&StoredMemory, f32)> = None;
for memory in &rule_memories {
let memory_embedding = embedder.embed_sync(&memory.content);
let Some(similarity) = cosine_similarity(&statement_embedding, &memory_embedding)
else {
continue;
};
let better = match &best {
None => similarity >= AGENTSMD_CONTRADICTION_SIMILARITY,
Some((current, current_similarity)) => {
similarity > *current_similarity
|| (similarity == *current_similarity && memory.id < current.id)
}
};
if better && similarity >= AGENTSMD_CONTRADICTION_SIMILARITY {
best = Some((memory, similarity));
}
}
let Some((memory, similarity)) = best else {
continue;
};
let Some((_, memory_polarity, _)) = classify_statement(&memory.content, true) else {
continue;
};
if memory_polarity == statement.polarity {
continue;
}
push_unique(
&mut report.suggested_commands,
format!("ee why {} --workspace . --json", memory.id),
);
report.contradictions.push(AgentsmdContradictionFinding {
line_number: statement.line_number,
file_text: statement.text.clone(),
file_polarity: statement.polarity.as_str(),
memory_id: memory.id.clone(),
memory_polarity: memory_polarity.as_str(),
similarity,
signal: "contradiction_link",
});
}
}
let primer = assemble_bridge_primer(connection, workspace_id, workspace_path, None)?;
let all_statements = parse_rule_statements(&content, None);
let duplicate_threshold = duplicate_similarity_threshold(workspace_path);
let memory_content_by_id: std::collections::BTreeMap<&str, &str> = memories
.iter()
.map(|memory| (memory.id.as_str(), memory.content.as_str()))
.collect();
if let Some(rules_section) = primer
.sections
.iter()
.find(|section| section.name == "rules")
{
let embedder = HashEmbedder::default_256();
for item in &rules_section.items {
let Some(memory_content) = memory_content_by_id.get(item.memory_id.as_str()) else {
continue;
};
let memory_embedding = embedder.embed_sync(memory_content);
let present = all_statements.iter().any(|statement| {
let statement_embedding = embedder.embed_sync(&statement.text);
cosine_similarity(&memory_embedding, &statement_embedding)
.is_some_and(|similarity| similarity >= duplicate_threshold)
});
if !present {
push_unique(
&mut report.suggested_commands,
"ee export agentsmd --workspace .".to_owned(),
);
report.missing_rules.push(AgentsmdMissingRuleFinding {
memory_id: item.memory_id.clone(),
line: item.line.clone(),
});
}
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
fn section(name: &str, lines: &[(&str, &str)]) -> PrimerSection {
PrimerSection {
name: name.to_owned(),
items: lines
.iter()
.map(|(memory_id, line)| crate::core::primer::PrimerItem {
memory_id: (*memory_id).to_owned(),
line: (*line).to_owned(),
level: "procedural".to_owned(),
kind: "rule".to_owned(),
confidence: 0.9,
provenance: Vec::new(),
})
.collect(),
}
}
#[test]
fn scan_finds_block_with_attributes() {
let content = "intro\n<!-- ee:agentsmd:begin generation=7 hash=blake3:abcd -->\nbody one\nbody two\n<!-- ee:agentsmd:end -->\ntail\n";
let ManagedBlockScan::Found(block) = scan_managed_block(content).expect("scan") else {
panic!("expected managed block");
};
assert_eq!(block.begin_index, 1);
assert_eq!(block.end_index, 4);
assert_eq!(block.generation, Some(7));
assert_eq!(block.recorded_hash.as_deref(), Some("blake3:abcd"));
assert_eq!(block.body, "body one\nbody two");
}
#[test]
fn scan_reports_missing_markers() {
assert_eq!(
scan_managed_block("no markers here\n").expect("scan"),
ManagedBlockScan::Missing
);
assert_eq!(
scan_managed_block("<!-- ee:agentsmd:beginning is just prose -->\n").expect("scan"),
ManagedBlockScan::Missing
);
}
#[test]
fn scan_refuses_malformed_marker_structures() {
assert!(scan_managed_block("<!-- ee:agentsmd:begin generation=1 -->\n").is_err());
assert!(scan_managed_block("<!-- ee:agentsmd:end -->\n").is_err());
let nested =
"<!-- ee:agentsmd:begin -->\n<!-- ee:agentsmd:begin -->\n<!-- ee:agentsmd:end -->\n";
assert!(scan_managed_block(nested).is_err());
let double = "<!-- ee:agentsmd:begin -->\n<!-- ee:agentsmd:end -->\n<!-- ee:agentsmd:begin -->\n<!-- ee:agentsmd:end -->\n";
assert!(scan_managed_block(double).is_err());
}
#[test]
fn rendered_block_round_trips_through_scan_with_matching_hash() {
let sections = vec![
section("rules", &[("mem_a", "Always run verify. [mem_a]")]),
section("warnings", &[("mem_b", "Goldens drift on Mac. [mem_b]")]),
section("decisions", &[("mem_c", "excluded section [mem_c]")]),
];
let body = render_managed_body(§ions);
assert!(body.contains("## Workspace rules (ee memory)"));
assert!(body.contains("## Workspace warnings (ee memory)"));
assert!(!body.contains("excluded section"));
let block = render_managed_block(&body, 42);
let ManagedBlockScan::Found(scanned) =
scan_managed_block(&format!("{block}\n")).expect("scan")
else {
panic!("expected managed block");
};
assert_eq!(scanned.generation, Some(42));
assert_eq!(
scanned.recorded_hash.as_deref(),
Some(managed_block_body_hash(&scanned.body).as_str()),
"recorded hash matches the scanned body"
);
}
#[test]
fn export_splice_preserves_bytes_outside_markers_bd_3just() {
let sections = vec![section("rules", &[("mem_a", "Always run verify. [mem_a]")])];
let body = render_managed_body(§ions);
let block = render_managed_block(&body, 7);
let suffix = "\n\n# hand notes\nno trailing newline";
let existing = format!("intro line\n\n{block}{suffix}");
let ManagedBlockScan::Found(found) = scan_managed_block(&existing).expect("scan") else {
panic!("expected managed block");
};
let (_old, spliced) = splice_managed_block(&existing, &found, &block);
assert_eq!(
spliced, existing,
"identical re-splice is byte-for-byte no-op; suffix without final newline preserved"
);
let crlf_existing = format!("intro\r\n\r\n{block}\r\ntail with crlf\r\nno final newline");
let ManagedBlockScan::Found(crlf_found) =
scan_managed_block(&crlf_existing).expect("scan crlf")
else {
panic!("expected managed block");
};
let (_crlf_old, crlf_spliced) = splice_managed_block(&crlf_existing, &crlf_found, &block);
assert_eq!(
crlf_spliced, crlf_existing,
"CRLF prefix/suffix preserved on identical re-splice"
);
assert!(
crlf_spliced.contains("tail with crlf\r\n"),
"CRLF terminator outside the markers not normalized to LF"
);
let new_sections = vec![section("rules", &[("mem_a", "Updated rule. [mem_a]")])];
let new_block = render_managed_block(&render_managed_body(&new_sections), 8);
let (_old3, changed) = splice_managed_block(&existing, &found, &new_block);
assert!(
changed.starts_with("intro line\n\n"),
"prefix preserved across a real block change"
);
assert!(
changed.ends_with(suffix),
"suffix (no trailing newline) preserved across a real block change"
);
assert!(changed.contains("Updated rule."), "new block body present");
assert!(
!changed.contains("Always run verify."),
"old block body replaced"
);
}
#[test]
fn render_is_deterministic_and_body_hash_detects_edits() {
let sections = vec![section("rules", &[("mem_a", "Always run verify. [mem_a]")])];
let body_one = render_managed_body(§ions);
let body_two = render_managed_body(§ions);
assert_eq!(body_one, body_two, "byte-identical re-render");
let edited = body_one.clone() + "sneaky hand edit\n";
assert_ne!(
managed_block_body_hash(&body_one),
managed_block_body_hash(&edited)
);
}
#[test]
fn classify_accepts_hard_modality_everywhere_and_cues_only_on_bullets() {
let must = "The release pipeline MUST run the verify script first.";
assert!(classify_statement(must, false).is_some());
let cue = "Never commit directly to the release branch here.";
assert!(classify_statement(cue, true).is_some());
assert!(
classify_statement(cue, false).is_none(),
"leading cues only count on bullets"
);
let soft = "Prefer structured logging over print statements.";
let (kind, polarity, _) = classify_statement(soft, true).expect("convention");
assert_eq!(kind, "convention");
assert_eq!(polarity, RulePolarity::Positive);
}
#[test]
fn classify_enforces_length_bounds() {
assert!(
classify_statement("MUST do it.", true).is_none(),
"too short"
);
let long = format!("ALWAYS {}", "x".repeat(AGENTSMD_RULE_MAX_CHARS));
assert!(classify_statement(&long, true).is_none(), "too long");
}
#[test]
fn parser_skips_fences_headings_tables_comments_and_managed_block() {
let content = "\
# Heading MUST NOT match here at all costs
- Always run the verify script before pushing changes.
| MUST not match inside a table row, ever |
> NEVER match inside a blockquote either, please.
<!-- NEVER match inside an html comment line. -->
```bash
echo 'NEVER match inside a fenced code block, period.'
```
<!-- ee:agentsmd:begin generation=1 hash=blake3:x -->
- NEVER match inside the managed block region.
<!-- ee:agentsmd:end -->
The deploy job MUST wait for the smoke suite to finish.
";
let exclude = match scan_managed_block(content).expect("scan") {
ManagedBlockScan::Found(block) => Some((block.begin_index, block.end_index)),
ManagedBlockScan::Missing => None,
};
let statements = parse_rule_statements(content, exclude);
let texts: Vec<&str> = statements
.iter()
.map(|statement| statement.text.as_str())
.collect();
assert_eq!(
texts,
vec![
"Always run the verify script before pushing changes.",
"The deploy job MUST wait for the smoke suite to finish.",
],
);
assert_eq!(statements[0].kind, "rule");
assert_eq!(statements[0].polarity, RulePolarity::Positive);
assert_eq!(statements[1].line_number, 16);
}
#[test]
fn parser_extracts_numbered_bullets_and_negative_modality() {
let content = "1. Do not regenerate goldens on the Mac checkout.\n2. plain step without any modality cue\n";
let statements = parse_rule_statements(content, None);
assert_eq!(statements.len(), 1);
assert_eq!(statements[0].polarity, RulePolarity::Negative);
assert_eq!(statements[0].modality, "Do not");
}
#[test]
fn block_diff_lists_removed_then_added_lines() {
let diff = render_block_diff("old line", "new one\nnew two");
assert_eq!(diff, "- old line\n+ new one\n+ new two\n");
}
#[test]
fn bridge_file_resolution_stays_inside_workspace() {
let workspace = Path::new("/workspace/project");
let (path, display) =
resolve_bridge_file(workspace, Some(Path::new("docs/CLAUDE.md"))).expect("resolve");
assert_eq!(path, PathBuf::from("/workspace/project/docs/CLAUDE.md"));
assert_eq!(display, "docs/CLAUDE.md");
for invalid in [
"",
"/tmp/AGENTS.md",
"../AGENTS.md",
"docs/../AGENTS.md",
"./AGENTS.md",
"docs/./AGENTS.md",
"C:\\tmp\\AGENTS.md",
"\\\\server\\share\\AGENTS.md",
] {
assert!(
resolve_bridge_file(workspace, Some(Path::new(invalid))).is_err(),
"{invalid:?} must not resolve as an agentsmd bridge file"
);
}
}
#[cfg(unix)]
#[test]
fn bridge_file_io_rejects_symlinked_components() -> Result<(), String> {
use std::os::unix::fs::symlink;
let root = std::env::temp_dir().join(format!(
"ee-agentsmd-symlink-{}-{}",
std::process::id(),
Utc::now().timestamp_nanos_opt().unwrap_or_default()
));
let workspace = root.join("workspace");
let outside = root.join("outside");
std::fs::create_dir_all(&workspace)
.map_err(|error| format!("failed to create workspace: {error}"))?;
std::fs::create_dir_all(&outside)
.map_err(|error| format!("failed to create outside dir: {error}"))?;
let outside_file = outside.join("AGENTS.md");
std::fs::write(&outside_file, "outside original")
.map_err(|error| format!("failed to seed outside file: {error}"))?;
symlink(&outside, workspace.join("linked"))
.map_err(|error| format!("failed to create symlink: {error}"))?;
let bridge_path = workspace.join("linked").join("AGENTS.md");
let display_path = "linked/AGENTS.md";
let read_error = read_bridge_file(&bridge_path, display_path)
.expect_err("read must reject a symlinked bridge path");
assert!(
read_error.message().contains("symlinked component"),
"unexpected read error: {}",
read_error.message()
);
let write_error = write_bridge_file(&bridge_path, "rewritten", display_path)
.expect_err("write must reject a symlinked bridge path");
assert!(
write_error.message().contains("symlinked component"),
"unexpected write error: {}",
write_error.message()
);
assert_eq!(
std::fs::read_to_string(&outside_file)
.map_err(|error| format!("failed to read outside file: {error}"))?,
"outside original",
"agentsmd bridge IO must not write through symlinked workspace paths"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn bridge_file_final_open_rejects_symlinked_leaf() -> Result<(), String> {
use std::os::unix::fs::symlink;
let root = std::env::temp_dir().join(format!(
"ee-agentsmd-leaf-symlink-{}-{}",
std::process::id(),
Utc::now().timestamp_nanos_opt().unwrap_or_default()
));
let workspace = root.join("workspace");
let outside = root.join("outside");
std::fs::create_dir_all(&workspace)
.map_err(|error| format!("failed to create workspace: {error}"))?;
std::fs::create_dir_all(&outside)
.map_err(|error| format!("failed to create outside dir: {error}"))?;
let outside_file = outside.join("AGENTS.md");
std::fs::write(&outside_file, "outside original")
.map_err(|error| format!("failed to seed outside file: {error}"))?;
let bridge_path = workspace.join("AGENTS.md");
symlink(&outside_file, &bridge_path)
.map_err(|error| format!("failed to create leaf symlink: {error}"))?;
open_bridge_file_for_read(&bridge_path)
.expect_err("final bridge read open must reject a symlinked leaf");
open_bridge_file_for_write(&bridge_path)
.expect_err("final bridge write open must reject a symlinked leaf");
assert!(
std::fs::symlink_metadata(&bridge_path)
.map_err(|error| format!("failed to inspect bridge symlink: {error}"))?
.file_type()
.is_symlink(),
"final open rejection must leave the symlink in place"
);
assert_eq!(
std::fs::read_to_string(&outside_file)
.map_err(|error| format!("failed to read outside file: {error}"))?,
"outside original",
"final bridge write open must not truncate or rewrite the symlink target"
);
Ok(())
}
#[test]
fn import_candidate_ids_are_deterministic_and_text_keyed() {
let first = import_candidate_id("wsp_1", "create_candidate", "rule", "AGENTS.md", "text");
let second = import_candidate_id("wsp_1", "create_candidate", "rule", "AGENTS.md", "text");
assert_eq!(first, second);
let moved_line_same_text =
import_candidate_id("wsp_1", "create_candidate", "rule", "AGENTS.md", "text");
assert_eq!(first, moved_line_same_text, "line moves do not duplicate");
let other = import_candidate_id("wsp_1", "create_candidate", "rule", "AGENTS.md", "other");
assert_ne!(first, other);
assert!(first.starts_with("curate_"));
}
}