use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::io::{self, Read};
use std::path::{Component, Path, PathBuf};
use std::time::SystemTime;
use chrono::{DateTime, Utc};
use serde::Serialize;
use serde_json::Value;
use crate::config::{
EnvVar, WORKSPACE_MARKER, WorkspaceDiagnostic, WorkspaceResolutionMode,
WorkspaceResolutionRequest, WorkspaceResolutionSource, WorkspaceScope, derive_workspace_scope,
diagnose_workspace_resolution, read_env_var, resolve_workspace,
};
use crate::core::hygiene_beads_state::{
BEADS_JSONL_MAX_INSPECT_BYTES, BEADS_JSONL_RELATIVE_PATH, BeadsClassification,
BeadsHygieneInputs, BeadsHygieneState, BeadsMetadataSignal, BeadsReservationHolder,
classify_beads_state,
};
use crate::core::hygiene_classifier::{
Bucket, ClassificationRow, HygieneClassifierConfig, Kind, SecretEvidenceLookup,
classify_workspace_with_config,
};
use crate::core::hygiene_coordination::{
AgentMailCoordinationInput, HygieneCoordinationOverlay, overlay_coordination_state,
path_matches_pattern, reservation_is_expired,
};
use crate::core::swarm_brief::{
AGENT_MAIL_SNAPSHOT_MAX_BYTES, SystemSwarmBriefCommandRunner, WorkspaceGitSnapshot,
WorkspaceGitSnapshotOptions, collect_workspace_git_snapshot, parse_agent_mail_snapshot_json,
validate_current_agent_mail_snapshot_for_workspace,
};
use crate::core::symbol_graph::SymbolGraphExtractor;
use crate::db::{
CreateAuditInput, CreateWorkspaceInput, DatabaseConfig, DbConnection, StoredWorkspace,
WorkspaceScopeFields, generate_audit_id,
};
use crate::models::degradation::{
WORKSPACE_HYGIENE_AGENT_MAIL_UNAVAILABLE_CODE, WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE,
WORKSPACE_HYGIENE_PARTIAL_METADATA_CODE, WORKSPACE_HYGIENE_SECRET_SCAN_SKIPPED_CODE,
};
use crate::models::{
DomainError, SymbolEvidenceLinkDegradationCode, SymbolEvidenceLinkSet,
SymbolEvidenceSourceKind, SymbolGraphDegradationCode, SymbolKind, SymbolRecord, SymbolSnapshot,
SymbolVisibility, WorkspaceId,
};
use crate::policy::{WORKSPACE_SECRET_RISK_DEFAULT_MAX_SCAN_BYTES, workspace_secret_risk_evidence};
use crate::runtime::determinism::{Deterministic, Seed};
pub const WORKSPACE_REGISTRY_SCHEMA_V1: &str = "ee.workspace.registry.v1";
pub const WORKSPACE_ALIAS_SCHEMA_V1: &str = "ee.workspace.alias.v1";
pub const WORKSPACE_RESOLVE_SCHEMA_V1: &str = "ee.workspace.resolve.v1";
pub const WORKSPACE_HYGIENE_SCHEMA_V1: &str = "ee.workspace_hygiene.v1";
pub const WORKSPACE_HYGIENE_SYMBOL_RISK_SCHEMA_V1: &str = "ee.workspace_hygiene.symbol_risk.v1";
pub const WORKSPACE_REGISTRY_ENV_VAR: &str = EnvVar::WorkspaceRegistry.name();
const WORKSPACE_ALIAS_SET_ACTION: &str = "workspace.alias.set";
const WORKSPACE_ALIAS_CLEAR_ACTION: &str = "workspace.alias.clear";
pub const WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS: usize = 10_000;
pub const WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST: usize = 10_000;
pub const WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP: usize = 10_000;
pub const WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_PATHS: usize = 20;
pub const WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_SYMBOLS_PER_PATH: usize = 8;
pub const WORKSPACE_HYGIENE_SECRET_SCAN_MAX_FILES: usize = 1_000;
pub const WORKSPACE_HYGIENE_SECRET_SCAN_MAX_TOTAL_BYTES: usize = 1_000_000;
pub const WORKSPACE_HYGIENE_AGENT_ADVISORY_TARGET_PRECOMMIT: &str = "precommit";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceListOptions {
pub registry_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceResolveOptions {
pub workspace_path: Option<PathBuf>,
pub target: Option<String>,
pub registry_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceAliasOptions {
pub workspace_path: Option<PathBuf>,
pub pick: Option<String>,
pub alias: Option<String>,
pub clear: bool,
pub dry_run: bool,
pub registry_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceHygieneOptions {
pub workspace_path: PathBuf,
pub self_agent_name: Option<String>,
pub agent_mail_snapshot_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceEntry {
pub workspace_id: String,
pub path: String,
pub alias: Option<String>,
pub scope_kind: String,
pub repository_root: Option<String>,
pub repository_fingerprint: Option<String>,
pub subproject_path: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl From<StoredWorkspace> for WorkspaceEntry {
fn from(workspace: StoredWorkspace) -> Self {
Self {
workspace_id: workspace.id,
path: workspace.path,
alias: workspace.name,
scope_kind: workspace.scope_kind,
repository_root: workspace.repository_root,
repository_fingerprint: workspace.repository_fingerprint,
subproject_path: workspace.subproject_path,
created_at: workspace.created_at,
updated_at: workspace.updated_at,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceListReport {
pub schema: &'static str,
pub command: &'static str,
pub registry_path: String,
pub registry_exists: bool,
pub workspaces: Vec<WorkspaceEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceAliasReport {
pub schema: &'static str,
pub command: &'static str,
pub status: &'static str,
pub registry_path: String,
pub workspace_id: String,
pub workspace_path: String,
pub alias: Option<String>,
pub previous_alias: Option<String>,
pub scope_kind: String,
pub repository_root: Option<String>,
pub repository_fingerprint: Option<String>,
pub subproject_path: Option<String>,
pub dry_run: bool,
pub persisted: bool,
pub audit_id: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceResolveReport {
pub schema: &'static str,
pub command: &'static str,
pub source: String,
pub target: Option<String>,
pub workspace_id: String,
pub root: String,
pub canonical_root: String,
pub marker_present: bool,
pub alias: Option<String>,
pub scope_kind: String,
pub repository_root: Option<String>,
pub repository_fingerprint: Option<String>,
pub subproject_path: Option<String>,
pub registry_path: String,
pub diagnostics: Vec<WorkspaceDiagnosticEntry>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneReport {
pub schema: &'static str,
pub command: &'static str,
pub read_only: bool,
#[serde(rename = "workspace")]
pub workspace_path: String,
#[serde(rename = "gitSummary")]
pub git_summary: WorkspaceHygieneGitSummary,
pub repository_root: String,
pub dirty_path_count: usize,
pub bucket_counts: Vec<WorkspaceHygieneCount>,
pub kind_counts: Vec<WorkspaceHygieneCount>,
#[serde(rename = "stagingRecommendations")]
pub staging_groups: Vec<WorkspaceHygieneStagingGroup>,
#[serde(rename = "pathClassifications")]
pub classifications: Vec<ClassificationRow>,
#[serde(skip)]
advisory_classifications: Vec<ClassificationRow>,
#[serde(rename = "doNotCommit")]
pub do_not_commit: Vec<String>,
#[serde(rename = "needsHumanReview")]
pub needs_human_review: Vec<String>,
#[serde(rename = "outputTruncation")]
pub output_truncation: WorkspaceHygieneOutputTruncation,
#[serde(rename = "secretScan")]
pub secret_scan: WorkspaceHygieneSecretScanReport,
#[serde(rename = "beadsState")]
pub beads_state: BeadsHygieneState,
#[serde(rename = "coordinationState")]
pub coordination: HygieneCoordinationOverlay,
#[serde(rename = "degraded")]
pub degraded_codes: Vec<&'static str>,
#[serde(rename = "nextActions")]
pub next_actions: Vec<String>,
#[serde(
rename = "agentHarnessAdvisory",
skip_serializing_if = "Option::is_none"
)]
pub agent_harness_advisory: Option<WorkspaceHygieneAgentHarnessAdvisory>,
#[serde(rename = "symbolRiskSummary", skip_serializing_if = "Option::is_none")]
pub symbol_risk_summary: Option<WorkspaceHygieneSymbolRiskSummary>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneAgentHarnessAdvisory {
pub schema: &'static str,
pub payload_schema: &'static str,
pub target: &'static str,
pub read_only: bool,
pub strict: bool,
pub status: &'static str,
pub recommended_exit_code: u8,
pub reason_count: usize,
pub reasons: Vec<WorkspaceHygieneAgentHarnessReason>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneSymbolRiskSummary {
pub schema: &'static str,
pub status: &'static str,
pub dirty_path_count: usize,
pub summarized_path_count: usize,
pub omitted_path_count: usize,
pub touched_symbol_count: usize,
pub high_risk_symbol_count: usize,
pub linked_evidence_count: usize,
pub recent_agent_activity_count: usize,
pub paths: Vec<WorkspaceHygieneSymbolRiskPath>,
pub degraded_codes: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneSymbolRiskPath {
pub path: String,
pub path_hash: String,
pub symbol_count: usize,
pub high_risk_symbol_count: usize,
pub linked_evidence_count: usize,
pub recent_agent_activity_count: usize,
pub symbols: Vec<WorkspaceHygieneSymbolRiskSymbol>,
pub agent_name_hashes: Vec<String>,
pub evidence_source_kinds: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneSymbolRiskSymbol {
pub symbol_id_hash: String,
pub canonical_name_hash: String,
pub kind: &'static str,
pub visibility: &'static str,
pub public_surface: bool,
pub start_line: u32,
pub end_line: u32,
pub linked_evidence_count: usize,
pub evidence_source_kinds: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceHygieneSymbolAgentActivity<'a> {
pub path: &'a str,
pub agent_name: &'a str,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneAgentHarnessReason {
pub code: &'static str,
pub category: &'static str,
pub message: String,
pub paths: Vec<String>,
pub repair: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneGitSummary {
pub repository_root: String,
pub dirty_path_count: usize,
pub bucket_counts: Vec<WorkspaceHygieneCount>,
pub kind_counts: Vec<WorkspaceHygieneCount>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneCount {
pub name: String,
pub count: usize,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneStagingGroup {
pub name: String,
pub paths: Vec<String>,
pub path_count: usize,
pub paths_truncated: bool,
pub omitted_path_count: usize,
pub kinds: Vec<String>,
pub reasons: Vec<String>,
pub recommendation: &'static str,
pub read_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneOutputTruncation {
pub truncated: bool,
pub max_path_classifications: usize,
pub max_paths_per_list: usize,
pub max_paths_per_staging_group: usize,
pub omitted_path_classifications: usize,
pub omitted_do_not_commit: usize,
pub omitted_needs_human_review: usize,
pub omitted_by_bucket: Vec<WorkspaceHygieneCount>,
pub omitted_by_kind: Vec<WorkspaceHygieneCount>,
pub staging_groups: Vec<WorkspaceHygieneStagingGroupTruncation>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneStagingGroupTruncation {
pub name: String,
pub omitted_path_count: usize,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneSecretScanReport {
pub read_only: bool,
pub scanned_file_count: usize,
pub scanned_byte_count: usize,
pub skipped_content_scan_count: usize,
pub max_files: usize,
pub max_file_bytes: usize,
pub max_total_bytes: usize,
}
struct WorkspaceHygieneReportInputs<'a> {
workspace_path: &'a Path,
snapshot: WorkspaceGitSnapshot,
classifier_config: &'a HygieneClassifierConfig,
jsonl_content: Option<&'a [u8]>,
self_agent_name: Option<&'a str>,
beads_metadata_signal: BeadsMetadataSignal,
beads_reservations: &'a [BeadsReservationHolder],
agent_mail_input: &'a AgentMailCoordinationInput,
now: DateTime<Utc>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct WorkspaceHygieneSecretScanBudget {
max_files: usize,
max_file_bytes: usize,
max_total_bytes: usize,
}
impl Default for WorkspaceHygieneSecretScanBudget {
fn default() -> Self {
Self {
max_files: WORKSPACE_HYGIENE_SECRET_SCAN_MAX_FILES,
max_file_bytes: WORKSPACE_SECRET_RISK_DEFAULT_MAX_SCAN_BYTES,
max_total_bytes: WORKSPACE_HYGIENE_SECRET_SCAN_MAX_TOTAL_BYTES,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct WorkspaceHygieneSecretScanSummary {
scanned_file_count: usize,
scanned_byte_count: usize,
skipped_content_scan_count: usize,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceDiagnosticEntry {
pub code: &'static str,
pub severity: &'static str,
pub message: String,
pub repair: String,
pub selected_source: Option<&'static str>,
pub selected_root: Option<String>,
pub conflicting_source: Option<&'static str>,
pub conflicting_root: Option<String>,
pub marker_roots: Vec<String>,
}
impl From<WorkspaceDiagnostic> for WorkspaceDiagnosticEntry {
fn from(diagnostic: WorkspaceDiagnostic) -> Self {
Self {
code: diagnostic.code,
severity: diagnostic.severity.as_str(),
message: diagnostic.message,
repair: diagnostic.repair,
selected_source: diagnostic
.selected_source
.map(WorkspaceResolutionSource::as_str),
selected_root: diagnostic
.selected_root
.map(|path| path.display().to_string()),
conflicting_source: diagnostic
.conflicting_source
.map(WorkspaceResolutionSource::as_str),
conflicting_root: diagnostic
.conflicting_root
.map(|path| path.display().to_string()),
marker_roots: diagnostic
.marker_roots
.into_iter()
.map(|path| path.display().to_string())
.collect(),
}
}
}
#[must_use]
pub fn registry_database_path_override(override_path: Option<&Path>) -> PathBuf {
if let Some(path) = override_path {
return path.to_path_buf();
}
if let Some(path) = read_env_var(EnvVar::WorkspaceRegistry) {
let trimmed = path.trim();
if !trimmed.is_empty() {
return PathBuf::from(trimmed);
}
}
if let Ok(xdg_data) = env::var("XDG_DATA_HOME") {
return PathBuf::from(xdg_data).join("ee").join("workspaces.db");
}
if let Ok(home) = env::var("HOME") {
return PathBuf::from(home)
.join(".local")
.join("share")
.join("ee")
.join("workspaces.db");
}
env::temp_dir().join("ee").join("workspaces.db")
}
#[must_use]
pub fn resolve_workspace_alias_for_cli(raw: &Path) -> Option<PathBuf> {
if looks_like_path(raw) {
return None;
}
let alias = raw.to_str()?;
let normalized = normalize_alias(alias).ok()?;
let registry_path = registry_database_path_override(None);
let row = find_alias_read_only(®istry_path, &normalized).ok()??;
Some(PathBuf::from(row.path))
}
pub fn list_workspace_registry(
options: &WorkspaceListOptions,
) -> Result<WorkspaceListReport, DomainError> {
let registry_path = registry_database_path_override(options.registry_path.as_deref());
if !registry_file_exists(®istry_path)? {
return Ok(WorkspaceListReport {
schema: WORKSPACE_REGISTRY_SCHEMA_V1,
command: "workspace list",
registry_path: registry_path.display().to_string(),
registry_exists: false,
workspaces: Vec::new(),
});
}
let conn = open_registry_read_only(®istry_path)?;
let workspaces = conn
.list_workspaces()
.map_err(|error| storage_error("failed to list workspace registry", error))?
.into_iter()
.map(WorkspaceEntry::from)
.collect();
Ok(WorkspaceListReport {
schema: WORKSPACE_REGISTRY_SCHEMA_V1,
command: "workspace list",
registry_path: registry_path.display().to_string(),
registry_exists: true,
workspaces,
})
}
pub fn resolve_workspace_report(
options: &WorkspaceResolveOptions,
) -> Result<WorkspaceResolveReport, DomainError> {
let registry_path = registry_database_path_override(options.registry_path.as_deref());
if let Some(target) = options.target.as_deref() {
if !looks_like_path(Path::new(target)) {
let alias = normalize_alias(target).map_err(alias_usage_error)?;
let row = find_alias_read_only(®istry_path, &alias)?.ok_or_else(|| {
DomainError::NotFound {
resource: "workspace alias".to_string(),
id: alias.clone(),
repair: Some("ee workspace list --json".to_string()),
}
})?;
return Ok(resolve_alias_row_report(®istry_path, target, row));
}
return resolve_path_report(®istry_path, Some(target), Some(PathBuf::from(target)));
}
resolve_path_report(®istry_path, None, options.workspace_path.clone())
}
pub const WORKSPACE_HYGIENE_SWARM_BRIEF_SUMMARY_SCHEMA_V1: &str =
"ee.workspace_hygiene.swarm_brief_summary.v1";
pub const WORKSPACE_HYGIENE_SWARM_BRIEF_TOP_PATHS_LIMIT: usize = 10;
pub const WORKSPACE_HYGIENE_SWARM_BRIEF_TOP_PATTERNS_LIMIT: usize = 5;
pub const WORKSPACE_HYGIENE_SWARM_BRIEF_COMMAND_HINT: &str = "ee workspace hygiene --json";
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceHygieneSwarmBriefSummary {
pub schema: &'static str,
pub status: &'static str,
pub dirty_path_count: usize,
pub bucket_counts: Vec<WorkspaceHygieneCount>,
pub kind_counts: Vec<WorkspaceHygieneCount>,
pub needs_human_review_top: Vec<String>,
pub needs_human_review_total: usize,
pub needs_human_review_truncated: bool,
pub coordination_blocker_count: usize,
pub coordination_blocker_patterns: Vec<String>,
pub beads_state_status: &'static str,
pub command_hint: &'static str,
pub degraded_codes: Vec<&'static str>,
#[serde(rename = "symbolRiskSummary", skip_serializing_if = "Option::is_none")]
pub symbol_risk_summary: Option<WorkspaceHygieneSymbolRiskSummary>,
}
impl WorkspaceHygieneSwarmBriefSummary {
#[must_use]
pub fn unavailable(degraded_code: &'static str) -> Self {
Self {
schema: WORKSPACE_HYGIENE_SWARM_BRIEF_SUMMARY_SCHEMA_V1,
status: "unavailable",
dirty_path_count: 0,
bucket_counts: Vec::new(),
kind_counts: Vec::new(),
needs_human_review_top: Vec::new(),
needs_human_review_total: 0,
needs_human_review_truncated: false,
coordination_blocker_count: 0,
coordination_blocker_patterns: Vec::new(),
beads_state_status: "unavailable",
command_hint: WORKSPACE_HYGIENE_SWARM_BRIEF_COMMAND_HINT,
degraded_codes: vec![degraded_code],
symbol_risk_summary: None,
}
}
#[must_use]
pub fn from_report(report: &WorkspaceHygieneReport) -> Self {
let visible_needs_human_review_count = report.needs_human_review.len();
let needs_total = visible_needs_human_review_count
.saturating_add(report.output_truncation.omitted_needs_human_review);
let needs_top: Vec<String> = report
.needs_human_review
.iter()
.take(WORKSPACE_HYGIENE_SWARM_BRIEF_TOP_PATHS_LIMIT)
.cloned()
.collect();
let needs_truncated = visible_needs_human_review_count > needs_top.len()
|| report.output_truncation.omitted_needs_human_review > 0;
let coordination_blocker_count = report.coordination.blocked_by_coordination.len();
let mut pattern_set: BTreeSet<String> = BTreeSet::new();
for blocked in &report.coordination.blocked_by_coordination {
if pattern_set.len() >= WORKSPACE_HYGIENE_SWARM_BRIEF_TOP_PATTERNS_LIMIT
&& !pattern_set.contains(&blocked.path_pattern)
{
continue;
}
pattern_set.insert(blocked.path_pattern.clone());
}
let coordination_blocker_patterns: Vec<String> = pattern_set.into_iter().collect();
let beads_state_status: &'static str = match report.beads_state.classification {
BeadsClassification::BeadsClean => "beads_clean",
BeadsClassification::BeadsExportOnly => "beads_export_only",
BeadsClassification::BeadsDbDirtyPendingFlush => "beads_db_dirty_pending_flush",
BeadsClassification::BeadsExternalChangesPendingImport => {
"beads_external_changes_pending_import"
}
BeadsClassification::BeadsConflictOrParseError => "beads_conflict_or_parse_error",
BeadsClassification::BeadsReservedByOtherAgent => "beads_reserved_by_other_agent",
BeadsClassification::BeadsLikelyCommitReady => "beads_likely_commit_ready",
};
let mut degraded_codes: Vec<&'static str> = Vec::new();
for code in report
.degraded_codes
.iter()
.chain(report.coordination.degraded_codes.iter())
.chain(report.beads_state.degraded_codes.iter())
.copied()
{
if !degraded_codes.contains(&code) {
degraded_codes.push(code);
}
}
Self {
schema: WORKSPACE_HYGIENE_SWARM_BRIEF_SUMMARY_SCHEMA_V1,
status: "available",
dirty_path_count: report.dirty_path_count,
bucket_counts: report.bucket_counts.clone(),
kind_counts: report.kind_counts.clone(),
needs_human_review_top: needs_top,
needs_human_review_total: needs_total,
needs_human_review_truncated: needs_truncated,
coordination_blocker_count,
coordination_blocker_patterns,
beads_state_status,
command_hint: WORKSPACE_HYGIENE_SWARM_BRIEF_COMMAND_HINT,
degraded_codes,
symbol_risk_summary: report.symbol_risk_summary.clone(),
}
}
}
#[must_use]
pub fn build_workspace_hygiene_swarm_brief_summary(
options: &WorkspaceHygieneOptions,
) -> WorkspaceHygieneSwarmBriefSummary {
match build_workspace_hygiene_report(options) {
Ok(report) => WorkspaceHygieneSwarmBriefSummary::from_report(&report),
Err(_) => {
WorkspaceHygieneSwarmBriefSummary::unavailable(WORKSPACE_HYGIENE_PARTIAL_METADATA_CODE)
}
}
}
pub fn build_workspace_hygiene_report(
options: &WorkspaceHygieneOptions,
) -> Result<WorkspaceHygieneReport, DomainError> {
let classifier_config = workspace_hygiene_classifier_config()?;
let snapshot_options = WorkspaceGitSnapshotOptions::for_workspace(&options.workspace_path);
let snapshot =
collect_workspace_git_snapshot(&snapshot_options, &SystemSwarmBriefCommandRunner)
.map_err(workspace_git_error)?;
let jsonl_content = read_bounded_file(
&options.workspace_path.join(BEADS_JSONL_RELATIVE_PATH),
BEADS_JSONL_MAX_INSPECT_BYTES + 1,
)
.ok();
let beads_metadata_signal = detect_beads_metadata_signal(&options.workspace_path);
let now = Utc::now();
let agent_mail_input = load_agent_mail_coordination_input(
options.agent_mail_snapshot_path.as_deref(),
&options.workspace_path,
now,
);
let beads_reservations = beads_reservations_from_agent_mail_input(&agent_mail_input, now);
let mut report = build_workspace_hygiene_report_from_inputs(WorkspaceHygieneReportInputs {
workspace_path: &options.workspace_path,
snapshot,
classifier_config: &classifier_config,
jsonl_content: jsonl_content.as_deref(),
self_agent_name: options.self_agent_name.as_deref(),
beads_metadata_signal,
beads_reservations: &beads_reservations,
agent_mail_input: &agent_mail_input,
now,
});
attach_workspace_hygiene_symbol_risk_summary_from_dirty_paths(
&mut report,
&options.workspace_path,
);
Ok(report)
}
fn build_workspace_hygiene_report_from_inputs(
inputs: WorkspaceHygieneReportInputs<'_>,
) -> WorkspaceHygieneReport {
let secret_scan_budget = WorkspaceHygieneSecretScanBudget::default();
let (secret_evidence, secret_scan) = workspace_hygiene_secret_evidence_with_budget(
inputs.workspace_path,
&inputs.snapshot,
secret_scan_budget,
);
let classifications_all = classify_workspace_with_config(
&inputs.snapshot,
&secret_evidence,
inputs.classifier_config,
);
let beads_state = classify_beads_state(BeadsHygieneInputs {
snapshot: &inputs.snapshot,
jsonl_content: inputs.jsonl_content,
self_agent_name: inputs.self_agent_name,
metadata_signal: inputs.beads_metadata_signal,
reservations: inputs.beads_reservations,
});
let coordination = overlay_coordination_state(
&classifications_all,
inputs.agent_mail_input,
inputs.now,
inputs.self_agent_name,
);
let bucket_counts = workspace_hygiene_bucket_counts(&classifications_all);
let kind_counts = workspace_hygiene_kind_counts(&classifications_all);
let staging_groups_all = workspace_hygiene_staging_groups(&classifications_all, &coordination);
let advisory_classifications = workspace_hygiene_advisory_classifications(&classifications_all);
let do_not_commit_all =
workspace_hygiene_paths_for_bucket(&classifications_all, Bucket::DoNotCommit);
let needs_human_review_all =
workspace_hygiene_paths_for_bucket(&classifications_all, Bucket::NeedsHumanReview);
let (classifications, omitted_path_classifications) =
workspace_hygiene_truncate_classifications(&classifications_all);
let (staging_groups, staging_group_truncations) =
workspace_hygiene_truncate_staging_groups(staging_groups_all);
let (do_not_commit, omitted_do_not_commit) =
workspace_hygiene_truncate_path_list(do_not_commit_all);
let (needs_human_review, omitted_needs_human_review) =
workspace_hygiene_truncate_path_list(needs_human_review_all);
let output_truncation = workspace_hygiene_output_truncation(
&classifications_all,
omitted_path_classifications,
omitted_do_not_commit,
omitted_needs_human_review,
staging_group_truncations,
);
let secret_scan = workspace_hygiene_secret_scan_report(secret_scan_budget, secret_scan);
let mut degraded_codes = workspace_hygiene_degraded_codes(&beads_state, &coordination);
if secret_scan.skipped_content_scan_count > 0 {
degraded_codes.push(WORKSPACE_HYGIENE_SECRET_SCAN_SKIPPED_CODE);
degraded_codes.sort_unstable();
degraded_codes.dedup();
}
if output_truncation.truncated
&& !degraded_codes.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE)
{
degraded_codes.push(WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE);
degraded_codes.sort_unstable();
degraded_codes.dedup();
}
let next_actions = workspace_hygiene_next_actions(
&staging_groups,
&do_not_commit,
&needs_human_review,
°raded_codes,
);
WorkspaceHygieneReport {
schema: WORKSPACE_HYGIENE_SCHEMA_V1,
command: "workspace hygiene",
read_only: true,
workspace_path: inputs.workspace_path.display().to_string(),
git_summary: WorkspaceHygieneGitSummary {
repository_root: inputs.snapshot.repository_root.clone(),
dirty_path_count: classifications_all.len(),
bucket_counts: bucket_counts.clone(),
kind_counts: kind_counts.clone(),
},
repository_root: inputs.snapshot.repository_root,
dirty_path_count: classifications_all.len(),
bucket_counts,
kind_counts,
staging_groups,
classifications,
advisory_classifications,
do_not_commit,
needs_human_review,
output_truncation,
secret_scan,
beads_state,
coordination,
degraded_codes,
next_actions,
agent_harness_advisory: None,
symbol_risk_summary: None,
}
}
pub fn build_workspace_hygiene_agent_harness_report(
options: &WorkspaceHygieneOptions,
strict: bool,
) -> Result<WorkspaceHygieneReport, DomainError> {
let mut report = build_workspace_hygiene_report(options)?;
attach_workspace_hygiene_agent_harness_advisory(&mut report, strict);
Ok(report)
}
pub fn attach_workspace_hygiene_agent_harness_advisory(
report: &mut WorkspaceHygieneReport,
strict: bool,
) {
report.agent_harness_advisory = Some(workspace_hygiene_agent_harness_advisory(report, strict));
}
pub fn attach_workspace_hygiene_symbol_risk_summary(
report: &mut WorkspaceHygieneReport,
symbol_snapshot: Option<&SymbolSnapshot>,
evidence_links: Option<&SymbolEvidenceLinkSet>,
recent_agent_activity: &[WorkspaceHygieneSymbolAgentActivity<'_>],
) {
report.symbol_risk_summary = Some(workspace_hygiene_symbol_risk_summary(
report,
symbol_snapshot,
evidence_links,
recent_agent_activity,
));
}
fn attach_workspace_hygiene_symbol_risk_summary_from_dirty_paths(
report: &mut WorkspaceHygieneReport,
workspace_path: &Path,
) {
let rust_paths = report
.classifications
.iter()
.map(|row| row.path.as_str())
.filter(|path| path.ends_with(".rs"))
.take(WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_PATHS)
.map(|path| workspace_path.join(path))
.collect::<Vec<_>>();
if rust_paths.is_empty() {
return;
}
let symbol_snapshot = SymbolGraphExtractor::default().extract_paths(workspace_path, rust_paths);
attach_workspace_hygiene_symbol_risk_summary(report, Some(&symbol_snapshot), None, &[]);
}
#[must_use]
pub fn workspace_hygiene_symbol_risk_summary(
report: &WorkspaceHygieneReport,
symbol_snapshot: Option<&SymbolSnapshot>,
evidence_links: Option<&SymbolEvidenceLinkSet>,
recent_agent_activity: &[WorkspaceHygieneSymbolAgentActivity<'_>],
) -> WorkspaceHygieneSymbolRiskSummary {
let dirty_paths = report
.classifications
.iter()
.map(|row| row.path.as_str())
.collect::<BTreeSet<_>>();
let Some(symbol_snapshot) = symbol_snapshot else {
return WorkspaceHygieneSymbolRiskSummary {
schema: WORKSPACE_HYGIENE_SYMBOL_RISK_SCHEMA_V1,
status: "unavailable",
dirty_path_count: dirty_paths.len(),
summarized_path_count: 0,
omitted_path_count: dirty_paths.len(),
touched_symbol_count: 0,
high_risk_symbol_count: 0,
linked_evidence_count: 0,
recent_agent_activity_count: 0,
paths: Vec::new(),
degraded_codes: vec!["symbol_snapshot_unavailable".to_owned()],
};
};
let mut symbols_by_path = BTreeMap::<&str, Vec<&SymbolRecord>>::new();
for symbol in &symbol_snapshot.symbols {
if dirty_paths.contains(symbol.path.as_str()) {
symbols_by_path
.entry(symbol.path.as_str())
.or_default()
.push(symbol);
}
}
for symbols in symbols_by_path.values_mut() {
symbols.sort_by(|left, right| {
(left.range.start_line, left.range.end_line, left.id.as_str()).cmp(&(
right.range.start_line,
right.range.end_line,
right.id.as_str(),
))
});
}
let mut evidence_by_path = BTreeMap::<&str, Vec<&crate::models::SymbolEvidenceLink>>::new();
let mut evidence_by_symbol = BTreeMap::<&str, Vec<&crate::models::SymbolEvidenceLink>>::new();
if let Some(link_set) = evidence_links {
for link in &link_set.links {
if dirty_paths.contains(link.target_path.as_str()) {
evidence_by_path
.entry(link.target_path.as_str())
.or_default()
.push(link);
if let Some(symbol_id) = link.symbol_id.as_deref() {
evidence_by_symbol.entry(symbol_id).or_default().push(link);
}
}
}
}
let mut activity_by_path = BTreeMap::<&str, BTreeSet<String>>::new();
for activity in recent_agent_activity {
if dirty_paths.contains(activity.path) {
activity_by_path
.entry(activity.path)
.or_default()
.insert(redaction_hash("agent", activity.agent_name));
}
}
let mut paths = Vec::new();
let mut touched_symbol_count = 0_usize;
let mut high_risk_symbol_count = 0_usize;
let mut linked_evidence_count = 0_usize;
let mut recent_agent_activity_count = 0_usize;
for path in dirty_paths
.iter()
.take(WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_PATHS)
{
let symbols = symbols_by_path.get(path).cloned().unwrap_or_default();
let path_evidence = evidence_by_path.get(path).cloned().unwrap_or_default();
let agent_name_hashes = activity_by_path
.get(path)
.map(|agents| agents.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
let evidence_source_kinds =
symbol_risk_evidence_source_kinds(path_evidence.iter().copied());
let mut symbol_rows = Vec::new();
let path_high_risk_count = symbols
.iter()
.filter(|symbol| symbol_is_high_risk_surface(symbol))
.count();
for symbol in symbols
.iter()
.copied()
.take(WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_SYMBOLS_PER_PATH)
{
let symbol_evidence = evidence_by_symbol
.get(symbol.id.as_str())
.cloned()
.unwrap_or_default();
let public_surface = symbol_is_high_risk_surface(symbol);
symbol_rows.push(WorkspaceHygieneSymbolRiskSymbol {
symbol_id_hash: redaction_hash("symbol_id", &symbol.id),
canonical_name_hash: redaction_hash("canonical_name", &symbol.canonical_name),
kind: symbol.kind.as_str(),
visibility: symbol_visibility_label(symbol.visibility),
public_surface,
start_line: symbol.range.start_line,
end_line: symbol.range.end_line,
linked_evidence_count: symbol_evidence.len(),
evidence_source_kinds: symbol_risk_evidence_source_kinds(
symbol_evidence.iter().copied(),
),
});
}
touched_symbol_count += symbols.len();
high_risk_symbol_count += symbols
.iter()
.filter(|symbol| symbol_is_high_risk_surface(symbol))
.count();
linked_evidence_count += path_evidence.len();
recent_agent_activity_count += agent_name_hashes.len();
paths.push(WorkspaceHygieneSymbolRiskPath {
path: (*path).to_owned(),
path_hash: redaction_hash("path", path),
symbol_count: symbols.len(),
high_risk_symbol_count: path_high_risk_count,
linked_evidence_count: path_evidence.len(),
recent_agent_activity_count: agent_name_hashes.len(),
symbols: symbol_rows,
agent_name_hashes,
evidence_source_kinds,
});
}
let omitted_path_count = dirty_paths
.len()
.saturating_sub(WORKSPACE_HYGIENE_SYMBOL_RISK_MAX_PATHS);
let mut degraded_codes = symbol_snapshot
.degraded
.iter()
.map(|item| symbol_graph_degradation_code(item.code).to_owned())
.collect::<BTreeSet<_>>();
if let Some(link_set) = evidence_links {
degraded_codes.extend(
link_set
.degraded
.iter()
.map(|item| symbol_evidence_link_degradation_code(item.code).to_owned()),
);
} else {
degraded_codes.insert("symbol_evidence_links_unavailable".to_owned());
}
if omitted_path_count > 0 {
degraded_codes.insert("symbol_risk_output_truncated".to_owned());
}
WorkspaceHygieneSymbolRiskSummary {
schema: WORKSPACE_HYGIENE_SYMBOL_RISK_SCHEMA_V1,
status: "available",
dirty_path_count: dirty_paths.len(),
summarized_path_count: paths.len(),
omitted_path_count,
touched_symbol_count,
high_risk_symbol_count,
linked_evidence_count,
recent_agent_activity_count,
paths,
degraded_codes: degraded_codes.into_iter().collect(),
}
}
#[must_use]
pub fn workspace_hygiene_agent_harness_advisory(
report: &WorkspaceHygieneReport,
strict: bool,
) -> WorkspaceHygieneAgentHarnessAdvisory {
let reasons = workspace_hygiene_agent_harness_reasons(report);
let recommended_exit_code = if strict && !reasons.is_empty() { 6 } else { 0 };
let status = match (strict, reasons.is_empty()) {
(_, true) => "ok",
(false, false) => "would_fail_strict",
(true, false) => "strict_failed",
};
WorkspaceHygieneAgentHarnessAdvisory {
schema: WORKSPACE_HYGIENE_SCHEMA_V1,
payload_schema: WORKSPACE_HYGIENE_SCHEMA_V1,
target: WORKSPACE_HYGIENE_AGENT_ADVISORY_TARGET_PRECOMMIT,
read_only: true,
strict,
status,
recommended_exit_code,
reason_count: reasons.len(),
reasons,
}
}
fn workspace_hygiene_agent_harness_reasons(
report: &WorkspaceHygieneReport,
) -> Vec<WorkspaceHygieneAgentHarnessReason> {
let mut reasons = Vec::new();
let secret_paths = workspace_hygiene_paths_for_kind(report, Kind::SecretRisk);
if !secret_paths.is_empty() {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "secret_risk",
category: "secret-risk",
message: "Workspace hygiene found dirty paths with secret-risk evidence.".to_string(),
paths: secret_paths,
repair: "Remove secrets, rotate exposed credentials if needed, and rerun `ee workspace hygiene --json`.",
});
}
if workspace_hygiene_is_scratch_only(report) {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "scratch_only_commit",
category: "scratch-only commit",
message: "The dirty set contains only scratch artifacts and has no commit-ready staging group.".to_string(),
paths: report.do_not_commit.clone(),
repair: "Leave scratch artifacts unstaged or get explicit human approval before committing them.",
});
}
let active_reservation_paths = workspace_hygiene_active_reservation_paths(report);
if !active_reservation_paths.is_empty() {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "active_reservation",
category: "active reservation",
message: "One or more dirty paths are covered by an active exclusive reservation held by another agent.".to_string(),
paths: active_reservation_paths,
repair: "Coordinate through Agent Mail, wait for the reservation to expire, or choose a disjoint commit slice.",
});
}
if workspace_hygiene_has_beads_conflict(report) {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "beads_conflict",
category: "Beads conflict",
message: format!(
"Beads metadata is not commit-ready: {}.",
report.beads_state.classification.as_str()
),
paths: vec![BEADS_JSONL_RELATIVE_PATH.to_string()],
repair: "Run `br sync --flush-only` or resolve the Beads import/export conflict before committing metadata.",
});
}
if let Some(line) = report.beads_state.parse_error_line {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "parse_error",
category: "parse error",
message: format!(
"Workspace hygiene could not parse Beads JSONL metadata near line {line}."
),
paths: vec![BEADS_JSONL_RELATIVE_PATH.to_string()],
repair: "Inspect `.beads/issues.jsonl` for malformed JSONL or conflict markers before committing.",
});
}
let binary_paths = workspace_hygiene_paths_for_kind(report, Kind::Binary);
if !binary_paths.is_empty() {
reasons.push(WorkspaceHygieneAgentHarnessReason {
code: "unknown_high_risk_binary",
category: "unknown high-risk binary",
message: "Workspace hygiene found dirty binary or oversized paths that need human review.".to_string(),
paths: binary_paths,
repair: "Inspect the binary artifacts manually and keep them out of the commit unless they are intentional.",
});
}
reasons
}
fn workspace_hygiene_paths_for_kind(report: &WorkspaceHygieneReport, kind: Kind) -> Vec<String> {
report
.classifications
.iter()
.chain(report.advisory_classifications.iter())
.filter(|row| row.kind == kind)
.map(|row| row.path.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn workspace_hygiene_is_scratch_only(report: &WorkspaceHygieneReport) -> bool {
let scratch_count = workspace_hygiene_count(&report.kind_counts, Kind::Scratch.as_str());
let do_not_commit_count =
workspace_hygiene_count(&report.bucket_counts, Bucket::DoNotCommit.as_str());
report.dirty_path_count > 0
&& scratch_count == report.dirty_path_count
&& do_not_commit_count == report.dirty_path_count
&& report.staging_groups.is_empty()
}
fn workspace_hygiene_active_reservation_paths(report: &WorkspaceHygieneReport) -> Vec<String> {
let mut paths = report
.coordination
.blocked_by_coordination
.iter()
.map(|blocked| blocked.path.clone())
.collect::<BTreeSet<_>>();
if report.beads_state.classification == BeadsClassification::BeadsReservedByOtherAgent {
paths.insert(BEADS_JSONL_RELATIVE_PATH.to_string());
}
paths.into_iter().collect()
}
fn workspace_hygiene_has_beads_conflict(report: &WorkspaceHygieneReport) -> bool {
matches!(
report.beads_state.classification,
BeadsClassification::BeadsConflictOrParseError
| BeadsClassification::BeadsDbDirtyPendingFlush
| BeadsClassification::BeadsExternalChangesPendingImport
)
}
fn symbol_is_high_risk_surface(symbol: &SymbolRecord) -> bool {
symbol.visibility != SymbolVisibility::Private
|| matches!(
symbol.kind,
SymbolKind::CliCommandHandler
| SymbolKind::JsonSchemaConstant
| SymbolKind::Trait
| SymbolKind::Enum
| SymbolKind::Struct
)
}
fn symbol_visibility_label(visibility: SymbolVisibility) -> &'static str {
match visibility {
SymbolVisibility::Public => "public",
SymbolVisibility::Restricted => "restricted",
SymbolVisibility::Private => "private",
}
}
fn symbol_risk_evidence_source_kinds<'a>(
links: impl IntoIterator<Item = &'a crate::models::SymbolEvidenceLink>,
) -> Vec<String> {
links
.into_iter()
.map(|link| symbol_evidence_source_kind(link.source_kind).to_owned())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn symbol_evidence_source_kind(kind: SymbolEvidenceSourceKind) -> &'static str {
kind.as_str()
}
fn symbol_graph_degradation_code(code: SymbolGraphDegradationCode) -> &'static str {
match code {
SymbolGraphDegradationCode::SourceMissing => "symbol_source_missing",
SymbolGraphDegradationCode::SourceNonRegular => "symbol_source_non_regular",
SymbolGraphDegradationCode::SourceTooLarge => "symbol_source_too_large",
SymbolGraphDegradationCode::SourceUnreadable => "symbol_source_unreadable",
SymbolGraphDegradationCode::SourceUnparsable => "symbol_source_unparsable",
SymbolGraphDegradationCode::SymbolIndexStale => "symbol_index_stale",
}
}
fn symbol_evidence_link_degradation_code(code: SymbolEvidenceLinkDegradationCode) -> &'static str {
match code {
SymbolEvidenceLinkDegradationCode::StaleLineSpan => "symbol_evidence_stale_line_span",
SymbolEvidenceLinkDegradationCode::SourceFileMissing => {
"symbol_evidence_source_file_missing"
}
SymbolEvidenceLinkDegradationCode::AmbiguousContainingSymbols => {
"symbol_evidence_ambiguous_containing_symbols"
}
SymbolEvidenceLinkDegradationCode::SymbolRenamed => "symbol_evidence_symbol_renamed",
SymbolEvidenceLinkDegradationCode::SymbolDeleted => "symbol_evidence_symbol_deleted",
}
}
fn redaction_hash(label: &str, value: &str) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(label.as_bytes());
hasher.update(&[0]);
hasher.update(&(value.len() as u64).to_be_bytes());
hasher.update(value.as_bytes());
format!("blake3:{}", hasher.finalize().to_hex())
}
fn load_agent_mail_coordination_input(
path: Option<&Path>,
workspace: &Path,
now: DateTime<Utc>,
) -> AgentMailCoordinationInput {
let Some(path) = path else {
return AgentMailCoordinationInput::Unavailable;
};
let Ok(contents) = read_agent_mail_snapshot(path) else {
return AgentMailCoordinationInput::Unavailable;
};
if agent_mail_snapshot_status_is_timeout(&contents) {
return AgentMailCoordinationInput::TimedOut;
}
if validate_current_agent_mail_snapshot_for_workspace(&contents, workspace, now).is_err() {
return AgentMailCoordinationInput::Unavailable;
}
let Ok(snapshot) = parse_agent_mail_snapshot_json(&contents) else {
return AgentMailCoordinationInput::Unavailable;
};
if !snapshot.degraded.is_empty() {
return AgentMailCoordinationInput::Unavailable;
}
let reservations = snapshot
.file_reservations
.into_iter()
.map(
|reservation| crate::core::hygiene_coordination::AgentMailReservation {
path_pattern: reservation.path_pattern,
holder_agent: reservation.holder,
exclusive: reservation.exclusive,
expires_at: reservation.expires_at,
reservation_id: None,
bead_id: None,
thread_id: None,
},
)
.collect();
let mut active_agents = snapshot
.agents
.into_iter()
.map(|agent| crate::core::hygiene_coordination::ActiveAgent {
name: agent.name,
last_active_at: agent.last_active_at,
})
.collect::<Vec<_>>();
active_agents.extend(parse_active_agents_from_snapshot(&contents));
active_agents.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| right.last_active_at.cmp(&left.last_active_at))
});
active_agents.dedup_by(|left, right| left.name == right.name);
AgentMailCoordinationInput::Available {
reservations,
active_agents,
}
}
fn beads_reservations_from_agent_mail_input(
input: &AgentMailCoordinationInput,
now: DateTime<Utc>,
) -> Vec<BeadsReservationHolder> {
let AgentMailCoordinationInput::Available { reservations, .. } = input else {
return Vec::new();
};
let mut beads_reservations = reservations
.iter()
.filter(|reservation| {
path_matches_pattern(BEADS_JSONL_RELATIVE_PATH, &reservation.path_pattern)
&& !reservation_is_expired(reservation, now)
})
.map(|reservation| BeadsReservationHolder {
agent_name: reservation.holder_agent.clone(),
exclusive: reservation.exclusive,
expires_ts_rfc3339: reservation.expires_at.clone().unwrap_or_default(),
})
.collect::<Vec<_>>();
beads_reservations.sort_by(|left, right| {
left.agent_name
.cmp(&right.agent_name)
.then_with(|| left.exclusive.cmp(&right.exclusive))
.then_with(|| left.expires_ts_rfc3339.cmp(&right.expires_ts_rfc3339))
});
beads_reservations
}
fn read_agent_mail_snapshot(path: &Path) -> io::Result<String> {
if let Some(symlink) = first_existing_symlink_component(path)? {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"refusing to read Agent Mail snapshot through symlink '{}'",
symlink.display()
),
));
}
let metadata = fs::symlink_metadata(path)?;
if !metadata.file_type().is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Agent Mail snapshot path '{}' is not a file",
path.display()
),
));
}
if metadata.len() > AGENT_MAIL_SNAPSHOT_MAX_BYTES as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Agent Mail snapshot '{}' exceeds the {AGENT_MAIL_SNAPSHOT_MAX_BYTES}-byte cap; refusing to read",
path.display()
),
));
}
let file = open_agent_mail_snapshot_for_read_no_follow(path)?;
let opened_metadata = file.metadata()?;
if !opened_metadata.file_type().is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Agent Mail snapshot path '{}' is not a file after open",
path.display()
),
));
}
if opened_metadata.len() > AGENT_MAIL_SNAPSHOT_MAX_BYTES as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Agent Mail snapshot '{}' grew past the {AGENT_MAIL_SNAPSHOT_MAX_BYTES}-byte cap after open; refusing to read",
path.display()
),
));
}
let read_limit = (AGENT_MAIL_SNAPSHOT_MAX_BYTES as u64).saturating_add(1);
let mut bytes = Vec::new();
file.take(read_limit).read_to_end(&mut bytes)?;
if bytes.len() > AGENT_MAIL_SNAPSHOT_MAX_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Agent Mail snapshot '{}' grew past the {AGENT_MAIL_SNAPSHOT_MAX_BYTES}-byte cap after the metadata check (TOCTOU); refusing to read",
path.display()
),
));
}
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
fn open_agent_mail_snapshot_for_read_no_follow(path: &Path) -> io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.read(true);
configure_agent_mail_snapshot_open_no_follow(&mut options);
options.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn configure_agent_mail_snapshot_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn configure_agent_mail_snapshot_open_no_follow(_options: &mut fs::OpenOptions) {}
fn first_existing_symlink_component(path: &Path) -> io::Result<Option<PathBuf>> {
let mut current = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(_) | Component::RootDir => {
current.push(component.as_os_str());
continue;
}
Component::CurDir => continue,
Component::ParentDir | Component::Normal(_) => {
current.push(component.as_os_str());
}
}
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => return Ok(Some(current)),
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
}
}
Ok(None)
}
fn agent_mail_snapshot_status_is_timeout(contents: &str) -> bool {
let Ok(value) = serde_json::from_str::<Value>(contents) else {
return false;
};
value
.get("status")
.or_else(|| value.get("agentMailStatus"))
.or_else(|| value.get("agent_mail_status"))
.and_then(Value::as_str)
.is_some_and(|status| matches!(status, "timed_out" | "timeout" | "timedOut"))
}
fn parse_active_agents_from_snapshot(
contents: &str,
) -> Vec<crate::core::hygiene_coordination::ActiveAgent> {
let Ok(value) = serde_json::from_str::<Value>(contents) else {
return Vec::new();
};
let Some(items) = value
.get("active_agents")
.or_else(|| value.get("activeAgents"))
.and_then(Value::as_array)
else {
return Vec::new();
};
let mut agents = items
.iter()
.filter_map(|item| {
let name = item
.get("name")
.or_else(|| item.get("agent_name"))
.or_else(|| item.get("agentName"))
.and_then(Value::as_str)?
.trim();
if name.is_empty() {
return None;
}
let last_active_at = item
.get("last_active_at")
.or_else(|| item.get("lastActiveAt"))
.or_else(|| item.get("last_active_ts"))
.or_else(|| item.get("lastActiveTs"))
.and_then(Value::as_str)
.map(str::to_owned);
Some(crate::core::hygiene_coordination::ActiveAgent {
name: name.to_owned(),
last_active_at,
})
})
.collect::<Vec<_>>();
agents.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.last_active_at.cmp(&right.last_active_at))
});
agents.dedup();
agents
}
fn workspace_hygiene_classifier_config() -> Result<HygieneClassifierConfig, DomainError> {
let generated = read_env_var(EnvVar::WorkspaceHygieneGeneratedPatterns);
let scratch = read_env_var(EnvVar::WorkspaceHygieneScratchPatterns);
let local_machine = read_env_var(EnvVar::WorkspaceHygieneLocalMachinePatterns);
let always_review = read_env_var(EnvVar::WorkspaceHygieneAlwaysReviewPatterns);
HygieneClassifierConfig::from_raw_pattern_values(
generated.as_deref(),
scratch.as_deref(),
local_machine.as_deref(),
always_review.as_deref(),
)
.map_err(|error| DomainError::Configuration {
message: format!("invalid workspace hygiene configuration: {error}"),
repair: Some(format!(
"Use matcher syntax like `{}=prefix:target/` or clear the invalid variable.",
EnvVar::WorkspaceHygieneGeneratedPatterns.name()
)),
})
}
fn read_bounded_file(path: &Path, max_bytes: usize) -> io::Result<Vec<u8>> {
let file = open_workspace_file_for_read_no_follow(path)?;
let metadata = file.metadata()?;
if !metadata.file_type().is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"workspace hygiene path '{}' is not a file after open",
path.display()
),
));
}
let mut buffer = Vec::new();
file.take(u64::try_from(max_bytes).unwrap_or(u64::MAX))
.read_to_end(&mut buffer)?;
Ok(buffer)
}
fn open_workspace_file_for_read_no_follow(path: &Path) -> io::Result<fs::File> {
let mut options = fs::OpenOptions::new();
options.read(true);
configure_workspace_open_no_follow(&mut options);
options.open(path)
}
#[cfg(all(unix, not(any(target_os = "espidf", target_os = "horizon"))))]
fn configure_workspace_open_no_follow(options: &mut fs::OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
}
#[cfg(not(all(unix, not(any(target_os = "espidf", target_os = "horizon")))))]
fn configure_workspace_open_no_follow(_options: &mut fs::OpenOptions) {}
fn workspace_hygiene_secret_evidence_with_budget(
workspace_path: &Path,
snapshot: &WorkspaceGitSnapshot,
budget: WorkspaceHygieneSecretScanBudget,
) -> (SecretEvidenceLookup, WorkspaceHygieneSecretScanSummary) {
let mut lookup = SecretEvidenceLookup::default();
let mut summary = WorkspaceHygieneSecretScanSummary::default();
for entry in &snapshot.entries {
let Some(metadata) = entry.metadata.as_ref() else {
continue;
};
if !metadata.exists || metadata.file_type != "file" {
continue;
}
let Some(size_bytes) = metadata.size_bytes else {
summary.skipped_content_scan_count += 1;
continue;
};
let Ok(size_bytes) = usize::try_from(size_bytes) else {
summary.skipped_content_scan_count += 1;
continue;
};
if metadata.large_file || size_bytes > budget.max_file_bytes {
summary.skipped_content_scan_count += 1;
continue;
}
if summary.scanned_file_count >= budget.max_files {
summary.skipped_content_scan_count += 1;
continue;
}
if summary
.scanned_byte_count
.checked_add(size_bytes)
.is_none_or(|total| total > budget.max_total_bytes)
{
summary.skipped_content_scan_count += 1;
continue;
}
let Some(full_path) = workspace_hygiene_safe_content_path(workspace_path, &entry.path)
else {
summary.skipped_content_scan_count += 1;
continue;
};
let bytes = match read_bounded_file(&full_path, budget.max_file_bytes.saturating_add(1)) {
Ok(bytes) if bytes.len() <= budget.max_file_bytes => bytes,
Ok(_) | Err(_) => {
summary.skipped_content_scan_count += 1;
continue;
}
};
if summary
.scanned_byte_count
.checked_add(bytes.len())
.is_none_or(|total| total > budget.max_total_bytes)
{
summary.skipped_content_scan_count += 1;
continue;
}
summary.scanned_file_count += 1;
summary.scanned_byte_count += bytes.len();
let report =
workspace_secret_risk_evidence(&entry.path, Some(&bytes), budget.max_file_bytes);
if report.skipped_content_scan {
summary.skipped_content_scan_count += 1;
}
if report.secret_risk {
lookup.insert(entry.path.clone(), report);
}
}
(lookup, summary)
}
fn workspace_hygiene_safe_content_path(
workspace_path: &Path,
relative_path: &str,
) -> Option<PathBuf> {
let path = Path::new(relative_path);
if path.components().any(|component| {
matches!(
component,
Component::Prefix(_) | Component::RootDir | Component::ParentDir
)
}) {
return None;
}
let full_path = workspace_path.join(path);
match first_existing_symlink_component(&full_path) {
Ok(None) => Some(full_path),
Ok(Some(_)) | Err(_) => None,
}
}
fn workspace_hygiene_secret_scan_report(
budget: WorkspaceHygieneSecretScanBudget,
summary: WorkspaceHygieneSecretScanSummary,
) -> WorkspaceHygieneSecretScanReport {
WorkspaceHygieneSecretScanReport {
read_only: true,
scanned_file_count: summary.scanned_file_count,
scanned_byte_count: summary.scanned_byte_count,
skipped_content_scan_count: summary.skipped_content_scan_count,
max_files: budget.max_files,
max_file_bytes: budget.max_file_bytes,
max_total_bytes: budget.max_total_bytes,
}
}
fn detect_beads_metadata_signal(workspace_path: &Path) -> BeadsMetadataSignal {
let beads_dir = workspace_path.join(".beads");
let jsonl_modified = modified_at(&beads_dir.join("issues.jsonl"));
let marker_modified =
newest_modified_at(&[beads_dir.join("beads.db"), beads_dir.join("last-touched")]);
match (marker_modified, jsonl_modified) {
(Some(_), None) => BeadsMetadataSignal::DbDirtyPendingFlush,
(Some(marker), Some(jsonl)) if marker > jsonl => BeadsMetadataSignal::DbDirtyPendingFlush,
(Some(marker), Some(jsonl)) if jsonl > marker => {
BeadsMetadataSignal::ExternalChangesPendingImport
}
_ => BeadsMetadataSignal::Unknown,
}
}
fn newest_modified_at(paths: &[PathBuf]) -> Option<SystemTime> {
paths.iter().filter_map(|path| modified_at(path)).max()
}
fn modified_at(path: &Path) -> Option<SystemTime> {
fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
}
fn workspace_hygiene_bucket_counts(rows: &[ClassificationRow]) -> Vec<WorkspaceHygieneCount> {
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for row in rows {
*counts.entry(row.bucket.as_str().to_owned()).or_default() += 1;
}
counts
.into_iter()
.map(|(name, count)| WorkspaceHygieneCount { name, count })
.collect()
}
fn workspace_hygiene_kind_counts(rows: &[ClassificationRow]) -> Vec<WorkspaceHygieneCount> {
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for row in rows {
*counts.entry(row.kind.as_str().to_owned()).or_default() += 1;
}
counts
.into_iter()
.map(|(name, count)| WorkspaceHygieneCount { name, count })
.collect()
}
fn workspace_hygiene_count(counts: &[WorkspaceHygieneCount], name: &str) -> usize {
counts
.iter()
.find(|count| count.name == name)
.map_or(0, |count| count.count)
}
fn workspace_hygiene_staging_groups(
rows: &[ClassificationRow],
coordination: &HygieneCoordinationOverlay,
) -> Vec<WorkspaceHygieneStagingGroup> {
let mut groups: BTreeMap<String, WorkspaceHygieneStagingGroupBuilder> = BTreeMap::new();
let blocked_paths = coordination
.blocked_by_coordination
.iter()
.map(|blocked| blocked.path.as_str())
.collect::<BTreeSet<_>>();
for row in rows {
if row.bucket != Bucket::StageCandidate {
continue;
}
if blocked_paths.contains(row.path.as_str()) {
continue;
}
let group = workspace_hygiene_stage_group_name(row);
groups.entry(group).or_default().push(row);
}
groups
.into_iter()
.map(|(name, builder)| builder.into_group(name))
.collect()
}
fn workspace_hygiene_stage_group_name(row: &ClassificationRow) -> String {
if row.path.starts_with("tests/fixtures/golden/")
|| row.path.starts_with("tests/fixtures/goldens/")
|| row.path.contains("/golden/")
|| row.path.contains("/goldens/")
{
return "goldens".to_owned();
}
match row.kind {
Kind::Source => row
.suggested_group
.clone()
.unwrap_or_else(|| "source".to_owned()),
Kind::Test => row
.suggested_group
.clone()
.unwrap_or_else(|| "tests".to_owned()),
Kind::Docs => "docs".to_owned(),
Kind::BeadsMetadata => "beads_metadata".to_owned(),
Kind::Generated => "generated".to_owned(),
Kind::Scratch => "scratch".to_owned(),
Kind::LocalMachine => "local_machine".to_owned(),
Kind::SecretRisk => "secret_risk".to_owned(),
Kind::Binary => "binary".to_owned(),
Kind::Unknown => "human_review".to_owned(),
}
}
#[derive(Default)]
struct WorkspaceHygieneStagingGroupBuilder {
paths: BTreeSet<String>,
kinds: BTreeSet<String>,
reasons: BTreeSet<String>,
}
impl WorkspaceHygieneStagingGroupBuilder {
fn push(&mut self, row: &ClassificationRow) {
self.paths.insert(row.path.clone());
self.kinds.insert(row.kind.as_str().to_owned());
self.reasons
.extend(row.reasons.iter().map(|reason| (*reason).to_owned()));
}
fn into_group(self, name: String) -> WorkspaceHygieneStagingGroup {
let paths = self.paths.into_iter().collect::<Vec<_>>();
let path_count = paths.len();
WorkspaceHygieneStagingGroup {
name,
paths,
path_count,
paths_truncated: false,
omitted_path_count: 0,
kinds: self.kinds.into_iter().collect(),
reasons: self.reasons.into_iter().collect(),
recommendation: "review_and_stage_as_one_logical_commit",
read_only: true,
}
}
}
fn workspace_hygiene_paths_for_bucket(rows: &[ClassificationRow], bucket: Bucket) -> Vec<String> {
let mut paths = BTreeSet::new();
for row in rows {
if row.bucket == bucket {
paths.insert(row.path.clone());
}
}
paths.into_iter().collect()
}
fn workspace_hygiene_advisory_classifications(
rows: &[ClassificationRow],
) -> Vec<ClassificationRow> {
rows.iter()
.filter(|row| matches!(row.kind, Kind::SecretRisk | Kind::Binary))
.cloned()
.collect()
}
fn workspace_hygiene_truncate_classifications(
rows: &[ClassificationRow],
) -> (Vec<ClassificationRow>, usize) {
let omitted = rows
.len()
.saturating_sub(WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS);
let mut visible = rows
.iter()
.take(WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS)
.cloned()
.collect::<Vec<_>>();
visible.shrink_to_fit();
(visible, omitted)
}
fn workspace_hygiene_truncate_path_list(paths: Vec<String>) -> (Vec<String>, usize) {
let original_len = paths.len();
let omitted = original_len.saturating_sub(WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST);
if omitted == 0 {
return (paths, 0);
}
(
paths
.into_iter()
.take(WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST)
.collect(),
omitted,
)
}
fn workspace_hygiene_truncate_staging_groups(
mut groups: Vec<WorkspaceHygieneStagingGroup>,
) -> (
Vec<WorkspaceHygieneStagingGroup>,
Vec<WorkspaceHygieneStagingGroupTruncation>,
) {
let mut truncations = Vec::new();
for group in &mut groups {
let original_len = group.paths.len();
let omitted = original_len.saturating_sub(WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP);
if omitted == 0 {
continue;
}
group
.paths
.truncate(WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP);
group.paths_truncated = true;
group.omitted_path_count = omitted;
group.path_count = original_len;
truncations.push(WorkspaceHygieneStagingGroupTruncation {
name: group.name.clone(),
omitted_path_count: omitted,
});
}
(groups, truncations)
}
fn workspace_hygiene_output_truncation(
full_rows: &[ClassificationRow],
omitted_path_classifications: usize,
omitted_do_not_commit: usize,
omitted_needs_human_review: usize,
staging_groups: Vec<WorkspaceHygieneStagingGroupTruncation>,
) -> WorkspaceHygieneOutputTruncation {
let truncated = omitted_path_classifications > 0
|| omitted_do_not_commit > 0
|| omitted_needs_human_review > 0
|| staging_groups
.iter()
.any(|group| group.omitted_path_count > 0);
let omitted_rows = if omitted_path_classifications == 0 {
&[][..]
} else {
&full_rows[WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS..]
};
WorkspaceHygieneOutputTruncation {
truncated,
max_path_classifications: WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS,
max_paths_per_list: WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST,
max_paths_per_staging_group: WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP,
omitted_path_classifications,
omitted_do_not_commit,
omitted_needs_human_review,
omitted_by_bucket: workspace_hygiene_bucket_counts(omitted_rows),
omitted_by_kind: workspace_hygiene_kind_counts(omitted_rows),
staging_groups,
}
}
fn workspace_hygiene_next_actions(
staging_groups: &[WorkspaceHygieneStagingGroup],
do_not_commit: &[String],
needs_human_review: &[String],
degraded_codes: &[&'static str],
) -> Vec<String> {
let mut actions = Vec::new();
if !staging_groups.is_empty() {
actions.push(
"Review stagingRecommendations and commit one logical group at a time.".to_string(),
);
}
if !needs_human_review.is_empty() {
actions.push("Inspect needsHumanReview paths before staging.".to_string());
}
if !do_not_commit.is_empty() {
actions.push(
"Leave doNotCommit paths unstaged unless a human explicitly overrides.".to_string(),
);
}
if degraded_codes.contains(&WORKSPACE_HYGIENE_AGENT_MAIL_UNAVAILABLE_CODE) {
actions.push(
"Refresh Agent Mail reservations before committing coordination-sensitive paths."
.to_string(),
);
}
if degraded_codes.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE) {
actions.push(
"Narrow the dirty path set or inspect JSON outputTruncation before staging large changes."
.to_string(),
);
}
actions
}
fn workspace_hygiene_degraded_codes(
beads_state: &BeadsHygieneState,
coordination: &HygieneCoordinationOverlay,
) -> Vec<&'static str> {
let mut codes: BTreeSet<&'static str> = BTreeSet::new();
codes.insert(WORKSPACE_HYGIENE_PARTIAL_METADATA_CODE);
codes.extend(beads_state.degraded_codes.iter().copied());
codes.extend(coordination.degraded_codes.iter().copied());
codes.into_iter().collect()
}
fn workspace_git_error(error: crate::core::swarm_brief::SwarmBriefCommandError) -> DomainError {
match error {
crate::core::swarm_brief::SwarmBriefCommandError::Unavailable(message)
| crate::core::swarm_brief::SwarmBriefCommandError::InvalidUtf8(message) => {
DomainError::Configuration {
message,
repair: Some("Run `ee workspace hygiene` inside a git checkout.".to_string()),
}
}
crate::core::swarm_brief::SwarmBriefCommandError::Failed { status, stderr, .. } => {
DomainError::Configuration {
message: format!(
"read-only git status collection failed with status {}: {}",
status
.map(|code| code.to_string())
.unwrap_or_else(|| "terminated_by_signal".to_string()),
stderr
),
repair: Some(
"Run `git status --porcelain=v2 --branch` to inspect the checkout.".to_string(),
),
}
}
crate::core::swarm_brief::SwarmBriefCommandError::TimedOut { timeout_ms } => {
DomainError::Configuration {
message: format!("read-only git status collection timed out after {timeout_ms} ms"),
repair: Some("Retry after any long-running git operation finishes.".to_string()),
}
}
}
}
pub fn alias_workspace(
options: &WorkspaceAliasOptions,
) -> Result<WorkspaceAliasReport, DomainError> {
if options.clear && options.alias.is_some() {
return Err(DomainError::Usage {
message: "--clear cannot be combined with --as or a positional alias".to_string(),
repair: Some(
"Use either `ee workspace alias --clear` or `ee workspace alias --as <name>`."
.to_string(),
),
});
}
let normalized_alias = options
.alias
.as_deref()
.map(normalize_alias)
.transpose()
.map_err(alias_usage_error)?;
if !options.clear && normalized_alias.is_none() {
return Err(DomainError::Usage {
message: "workspace alias requires --as <name> or --clear".to_string(),
repair: Some("ee workspace alias --help".to_string()),
});
}
let registry_path = registry_database_path_override(options.registry_path.as_deref());
let target = resolve_alias_target(®istry_path, options)?;
let previous_alias = target.name.clone();
if let Some(alias) = normalized_alias.as_deref() {
ensure_alias_available(®istry_path, alias, &target.id)?;
}
if options.dry_run {
return Ok(WorkspaceAliasReport {
schema: WORKSPACE_ALIAS_SCHEMA_V1,
command: "workspace alias",
status: if options.clear {
"would_clear"
} else {
"would_set"
},
registry_path: registry_path.display().to_string(),
workspace_id: target.id,
workspace_path: target.path,
alias: normalized_alias,
previous_alias,
scope_kind: target.scope_kind,
repository_root: target.repository_root,
repository_fingerprint: target.repository_fingerprint,
subproject_path: target.subproject_path,
dry_run: true,
persisted: false,
audit_id: None,
});
}
let conn = open_registry_write(®istry_path)?;
let action = if options.clear {
WORKSPACE_ALIAS_CLEAR_ACTION
} else {
WORKSPACE_ALIAS_SET_ACTION
};
let audit_id = generate_audit_id();
let target_id = target.id.clone();
let target_path = target.path.clone();
let target_scope_kind = target.scope_kind.clone();
let target_repository_root = target.repository_root.clone();
let target_repository_fingerprint = target.repository_fingerprint.clone();
let target_subproject_path = target.subproject_path.clone();
let alias_for_write = normalized_alias.clone();
let details = serde_json::json!({
"schema": WORKSPACE_ALIAS_SCHEMA_V1,
"workspaceId": target_id,
"workspacePath": target_path,
"previousAlias": previous_alias,
"alias": alias_for_write,
"scopeKind": target_scope_kind,
"repositoryRoot": target_repository_root,
"repositoryFingerprint": target_repository_fingerprint,
"subprojectPath": target_subproject_path,
"dryRun": false
})
.to_string();
conn.with_transaction(|| {
upsert_workspace_row(&conn, &target)?;
conn.update_workspace_name(&target.id, alias_for_write.as_deref())?;
conn.insert_audit(
&audit_id,
&CreateAuditInput {
workspace_id: Some(target.id.clone()),
actor: Some("ee-cli".to_string()),
action: action.to_string(),
target_type: Some("workspace".to_string()),
target_id: Some(target.id.clone()),
details: Some(details),
},
)?;
Ok(())
})
.map_err(|error| storage_error("failed to persist workspace alias", error))?;
Ok(WorkspaceAliasReport {
schema: WORKSPACE_ALIAS_SCHEMA_V1,
command: "workspace alias",
status: if options.clear { "cleared" } else { "set" },
registry_path: registry_path.display().to_string(),
workspace_id: target.id,
workspace_path: target.path,
alias: normalized_alias,
previous_alias,
scope_kind: target.scope_kind,
repository_root: target.repository_root,
repository_fingerprint: target.repository_fingerprint,
subproject_path: target.subproject_path,
dry_run: false,
persisted: true,
audit_id: Some(audit_id),
})
}
fn resolve_path_report(
registry_path: &Path,
target: Option<&str>,
workspace_path: Option<PathBuf>,
) -> Result<WorkspaceResolveReport, DomainError> {
let request = WorkspaceResolutionRequest::from_process(
workspace_path,
WorkspaceResolutionMode::AllowUninitialized,
)
.map_err(|error| DomainError::Configuration {
message: error.to_string(),
repair: Some("Run from a readable directory or pass --workspace .".to_string()),
})?;
let resolution = resolve_workspace(&request).map_err(|error| DomainError::Configuration {
message: error.to_string(),
repair: Some("ee init --workspace .".to_string()),
})?;
let diagnostics = diagnose_workspace_resolution(&request, &resolution)
.into_iter()
.map(WorkspaceDiagnosticEntry::from)
.collect();
let registry_alias = find_workspace_alias_read_only(registry_path, &resolution.location.root)?;
let requested_id = stable_workspace_id(&resolution.canonical_root);
let database = resolution
.location
.root
.join(WORKSPACE_MARKER)
.join("ee.db");
let stored = if registry_file_exists(&database)? {
let database_error = |error: crate::db::DbError| DomainError::Storage {
message: format!(
"Failed to read workspace database '{}': {error}",
database.display()
),
repair: Some(format!(
"ee doctor --workspace {} --json",
shell_quote_path_arg(&resolution.location.root)
)),
};
let connection = DbConnection::open_file_read_only(&database).map_err(database_error)?;
if connection.needs_migration().map_err(database_error)? {
return Err(DomainError::MigrationRequired {
message: format!(
"Workspace database requires migration: {}",
database.display()
),
repair: Some(format!(
"ee migrate run --workspace {}",
shell_quote_path_arg(&resolution.location.root)
)),
});
}
select_existing_workspace_row(
&connection,
&requested_id,
&[&resolution.location.root, &resolution.canonical_root],
)?
} else {
None
};
let scope = stored.as_ref().map_or_else(
|| workspace_scope_fields(&resolution.scope),
|row| WorkspaceScopeFields {
scope_kind: row.scope_kind.clone(),
repository_root: row.repository_root.clone(),
repository_fingerprint: row.repository_fingerprint.clone(),
subproject_path: row.subproject_path.clone(),
},
);
let workspace_id = stored.as_ref().map_or(requested_id, |row| row.id.clone());
let alias = registry_alias.or_else(|| stored.and_then(|row| row.name));
Ok(WorkspaceResolveReport {
schema: WORKSPACE_RESOLVE_SCHEMA_V1,
command: "workspace resolve",
source: resolution.source.as_str().to_string(),
target: target.map(str::to_string),
workspace_id,
root: resolution.location.root.display().to_string(),
canonical_root: resolution.canonical_root.display().to_string(),
marker_present: resolution.marker_present,
alias,
scope_kind: scope.scope_kind,
repository_root: scope.repository_root,
repository_fingerprint: scope.repository_fingerprint,
subproject_path: scope.subproject_path,
registry_path: registry_path.display().to_string(),
diagnostics,
})
}
fn resolve_alias_row_report(
registry_path: &Path,
target: &str,
row: StoredWorkspace,
) -> WorkspaceResolveReport {
let root = PathBuf::from(&row.path);
let canonical_root = canonical_or_lexical(&root);
WorkspaceResolveReport {
schema: WORKSPACE_RESOLVE_SCHEMA_V1,
command: "workspace resolve",
source: "alias".to_string(),
target: Some(target.to_string()),
workspace_id: row.id,
root: root.display().to_string(),
canonical_root: canonical_root.display().to_string(),
marker_present: root.join(WORKSPACE_MARKER).is_dir(),
alias: row.name,
scope_kind: row.scope_kind,
repository_root: row.repository_root,
repository_fingerprint: row.repository_fingerprint,
subproject_path: row.subproject_path,
registry_path: registry_path.display().to_string(),
diagnostics: Vec::new(),
}
}
fn resolve_alias_target(
registry_path: &Path,
options: &WorkspaceAliasOptions,
) -> Result<StoredWorkspace, DomainError> {
if let Some(pick) = options.pick.as_deref() {
if pick.starts_with("wsp_") {
if let Some(row) = find_workspace_id_read_only(registry_path, pick)? {
return Ok(row);
}
return Err(DomainError::NotFound {
resource: "workspace".to_string(),
id: pick.to_string(),
repair: Some("ee workspace list --json".to_string()),
});
}
return workspace_row_for_path(pick);
}
let selected = options
.workspace_path
.clone()
.unwrap_or_else(|| PathBuf::from("."));
workspace_row_for_path(&selected.display().to_string())
}
fn workspace_row_for_path(raw: &str) -> Result<StoredWorkspace, DomainError> {
let root = lexical_absolute(
&env::current_dir().map_err(|error| DomainError::Configuration {
message: format!("failed to read current directory: {error}"),
repair: Some("Run from a readable directory or pass --workspace .".to_string()),
})?,
Path::new(raw),
);
if !root.exists() {
return Err(DomainError::Configuration {
message: format!("workspace path does not exist: {}", root.display()),
repair: Some("Create the directory or pass an existing --workspace path.".to_string()),
});
}
let marker = root.join(WORKSPACE_MARKER);
if !marker.is_dir() {
return Err(DomainError::Configuration {
message: format!("workspace is not initialized: {}", root.display()),
repair: Some(format!(
"ee init --workspace {}",
shell_quote_path_arg(&root)
)),
});
}
let canonical = canonical_or_lexical(&root);
let path = canonical.display().to_string();
let scope = workspace_scope_fields(&derive_workspace_scope(&canonical));
Ok(StoredWorkspace {
id: stable_workspace_id(&canonical),
path,
name: None,
scope_kind: scope.scope_kind,
repository_root: scope.repository_root,
repository_fingerprint: scope.repository_fingerprint,
subproject_path: scope.subproject_path,
created_at: String::new(),
updated_at: String::new(),
})
}
fn shell_quote_path_arg(path: &Path) -> String {
let path_text = path.to_string_lossy();
shell_quote_command_arg(path_text.as_ref())
}
fn shell_quote_command_arg(value: &str) -> String {
if value.is_empty() {
return "''".to_owned();
}
if value.bytes().all(|byte| {
matches!(
byte,
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'_'
| b'-'
| b'.'
| b'/'
| b':'
| b'@'
| b'+'
| b'='
)
}) {
value.to_owned()
} else {
format!("'{}'", value.replace('\'', "'\\''"))
}
}
fn workspace_scope_fields(scope: &WorkspaceScope) -> WorkspaceScopeFields {
WorkspaceScopeFields {
scope_kind: scope.kind.as_str().to_string(),
repository_root: scope
.repository_root
.as_ref()
.map(|path| path.display().to_string()),
repository_fingerprint: scope.repository_fingerprint.clone(),
subproject_path: scope
.subproject_path
.as_ref()
.map(|path| path.display().to_string()),
}
}
fn upsert_workspace_row(conn: &DbConnection, target: &StoredWorkspace) -> crate::db::Result<()> {
let scope = WorkspaceScopeFields {
scope_kind: target.scope_kind.clone(),
repository_root: target.repository_root.clone(),
repository_fingerprint: target.repository_fingerprint.clone(),
subproject_path: target.subproject_path.clone(),
};
conn.upsert_workspace_with_scope(
&target.id,
&CreateWorkspaceInput {
path: target.path.clone(),
name: target.name.clone(),
},
&scope,
)
}
fn ensure_alias_available(
registry_path: &Path,
alias: &str,
workspace_id: &str,
) -> Result<(), DomainError> {
if let Some(existing) = find_alias_read_only(registry_path, alias)? {
if existing.id != workspace_id {
return Err(DomainError::Usage {
message: format!(
"workspace alias `{alias}` already points to {}",
existing.path
),
repair: Some(
"Choose a different alias or clear the existing workspace alias first."
.to_string(),
),
});
}
}
Ok(())
}
fn find_workspace_alias_read_only(
registry_path: &Path,
workspace_path: &Path,
) -> Result<Option<String>, DomainError> {
if !registry_file_exists(registry_path)? {
return Ok(None);
}
let canonical = canonical_or_lexical(workspace_path);
let conn = open_registry_read_only(registry_path)?;
Ok(select_existing_workspace_row(
&conn,
&stable_workspace_id(&canonical),
&[workspace_path, canonical.as_path()],
)?
.and_then(|row| row.name))
}
fn find_alias_read_only(
registry_path: &Path,
alias: &str,
) -> Result<Option<StoredWorkspace>, DomainError> {
if !registry_file_exists(registry_path)? {
return Ok(None);
}
let conn = open_registry_read_only(registry_path)?;
let rows = conn
.list_workspaces()
.map_err(|error| storage_error("failed to query workspace aliases", error))?;
Ok(rows
.into_iter()
.find(|row| row.name.as_deref() == Some(alias)))
}
fn find_workspace_id_read_only(
registry_path: &Path,
workspace_id: &str,
) -> Result<Option<StoredWorkspace>, DomainError> {
if !registry_file_exists(registry_path)? {
return Ok(None);
}
let conn = open_registry_read_only(registry_path)?;
conn.get_workspace(workspace_id)
.map_err(|error| storage_error("failed to query workspace registry", error))
}
fn open_registry_read_only(registry_path: &Path) -> Result<DbConnection, DomainError> {
if !registry_file_exists(registry_path)? {
return Err(DomainError::Storage {
message: format!("workspace registry not found: {}", registry_path.display()),
repair: Some(
"Run `ee workspace alias --as <name>` to create the registry.".to_string(),
),
});
}
let conn = DbConnection::open_file_read_only(registry_path)
.map_err(|error| storage_error("failed to open workspace registry", error))?;
if conn
.needs_migration()
.map_err(|error| storage_error("failed to inspect workspace registry schema", error))?
{
return Err(DomainError::MigrationRequired {
message: format!(
"workspace registry requires migration: {}",
registry_path.display()
),
repair: Some("Run a mutating workspace registry command such as `ee workspace alias --as <name>`.".to_string()),
});
}
Ok(conn)
}
fn open_registry_write(registry_path: &Path) -> Result<DbConnection, DomainError> {
ensure_registry_path_has_no_symlink_components(registry_path)?;
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent).map_err(|error| DomainError::Storage {
message: format!(
"failed to create workspace registry directory {}: {error}",
parent.display()
),
repair: Some(
"Check permissions or set EE_WORKSPACE_REGISTRY to a writable path.".to_string(),
),
})?;
}
ensure_registry_path_has_no_symlink_components(registry_path)?;
ensure_existing_registry_path_is_regular_file(registry_path)?;
let conn = DbConnection::open(DatabaseConfig::file(registry_path))
.map_err(|error| storage_error("failed to open workspace registry", error))?;
conn.migrate()
.map_err(|error| storage_error("failed to migrate workspace registry", error))?;
Ok(conn)
}
fn normalize_alias(raw: &str) -> Result<String, String> {
let alias = raw.trim();
if alias.is_empty() {
return Err("workspace alias cannot be empty".to_string());
}
if alias == "." || alias == ".." {
return Err("workspace alias cannot be `.` or `..`".to_string());
}
if alias.starts_with('.') {
return Err("workspace alias cannot start with `.`".to_string());
}
if alias.len() > 64 {
return Err("workspace alias cannot exceed 64 bytes".to_string());
}
if alias
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.'))
{
return Err(
"workspace alias may only contain ASCII letters, numbers, dots, dashes, and underscores"
.to_string(),
);
}
Ok(alias.to_string())
}
fn ensure_registry_path_has_no_symlink_components(path: &Path) -> Result<(), DomainError> {
match registry_path_has_symlink_component(path) {
Ok(false) => Ok(()),
Ok(true) => Err(DomainError::Storage {
message: format!(
"refusing workspace registry path with symlink component: {}",
path.display()
),
repair: Some(
"Set EE_WORKSPACE_REGISTRY to a non-symlinked path under a trusted directory."
.to_string(),
),
}),
Err(error) => Err(DomainError::Storage {
message: format!(
"failed to inspect workspace registry path {}: {error}",
path.display()
),
repair: Some(
"Check permissions or set EE_WORKSPACE_REGISTRY to a readable path.".to_string(),
),
}),
}
}
fn registry_file_exists(path: &Path) -> Result<bool, DomainError> {
ensure_registry_path_has_no_symlink_components(path)?;
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => Ok(true),
Ok(_) => Err(non_regular_registry_path_error(path)),
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
Ok(false)
}
Err(error) => Err(DomainError::Storage {
message: format!(
"failed to inspect workspace registry path {}: {error}",
path.display()
),
repair: Some(
"Check permissions or set EE_WORKSPACE_REGISTRY to a readable path.".to_string(),
),
}),
}
}
fn ensure_existing_registry_path_is_regular_file(path: &Path) -> Result<(), DomainError> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_file() => Ok(()),
Ok(_) => Err(non_regular_registry_path_error(path)),
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
Ok(())
}
Err(error) => Err(DomainError::Storage {
message: format!(
"failed to inspect workspace registry path {}: {error}",
path.display()
),
repair: Some(
"Check permissions or set EE_WORKSPACE_REGISTRY to a readable path.".to_string(),
),
}),
}
}
fn non_regular_registry_path_error(path: &Path) -> DomainError {
DomainError::Storage {
message: format!(
"workspace registry path is not a regular file: {}",
path.display()
),
repair: Some(
"Set EE_WORKSPACE_REGISTRY to a regular database file path or move the directory aside."
.to_string(),
),
}
}
fn registry_path_has_symlink_component(path: &Path) -> io::Result<bool> {
crate::core::path_safety::path_has_symlink_component(path)
}
fn alias_usage_error(message: String) -> DomainError {
DomainError::Usage {
message,
repair: Some("Use an alias like `project-main`, `client.api`, or `repo_1`.".to_string()),
}
}
fn storage_error(context: &str, error: crate::db::DbError) -> DomainError {
DomainError::Storage {
message: format!("{context}: {error}"),
repair: Some(
"Run `ee doctor --json` and verify the workspace registry database.".to_string(),
),
}
}
fn looks_like_path(path: &Path) -> bool {
if path.is_absolute() {
return true;
}
let rendered = path.to_string_lossy();
rendered.starts_with('.')
|| rendered.starts_with('~')
|| rendered.contains('/')
|| rendered.contains('\\')
}
fn canonical_or_lexical(path: &Path) -> PathBuf {
path.canonicalize()
.unwrap_or_else(|_| lexical_absolute(Path::new("."), path))
}
fn lexical_absolute(base: &Path, path: &Path) -> PathBuf {
let joined = if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
};
normalize_lexical(&joined)
}
fn normalize_lexical(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(prefix) => out.push(prefix.as_os_str()),
Component::RootDir => out.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() && !path.is_absolute() {
out.push("..");
}
}
Component::Normal(segment) => out.push(segment),
}
}
out
}
fn strip_windows_verbatim_prefix(rendered: &str) -> String {
if let Some(rest) = rendered.strip_prefix(r"\\?\UNC\") {
return format!(r"\\{rest}");
}
if let Some(rest) = rendered.strip_prefix(r"\\?\") {
return rest.to_owned();
}
rendered.to_owned()
}
pub fn stable_workspace_id(path: &Path) -> String {
let rendered = strip_windows_verbatim_prefix(&path.to_string_lossy());
let hash = blake3::hash(format!("workspace:{rendered}").as_bytes());
let mut bytes = [0_u8; 16];
for (target, source) in bytes.iter_mut().zip(hash.as_bytes().iter().copied()) {
*target = source;
}
WorkspaceId::from_uuid(uuid::Uuid::from_bytes(bytes)).to_string()
}
pub(crate) fn ensure_bound_workspace(
connection: &DbConnection,
requested_workspace_id: &str,
workspace_paths: &[&Path],
) -> Result<String, DomainError> {
if let Some(existing) =
select_existing_workspace_row(connection, requested_workspace_id, workspace_paths)?
{
return Ok(existing.id);
}
let path = workspace_paths
.first()
.copied()
.unwrap_or_else(|| Path::new("."))
.to_string_lossy()
.into_owned();
let input = CreateWorkspaceInput {
path,
name: workspace_paths.first().copied().and_then(|workspace_path| {
workspace_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
}),
};
let scope = workspace_scope_fields(&derive_workspace_scope(Path::new(&input.path)));
connection
.upsert_workspace_with_scope(requested_workspace_id, &input, &scope)
.map_err(|error| DomainError::Storage {
message: format!("Failed to register workspace: {error}"),
repair: Some("ee doctor".to_owned()),
})?;
Ok(select_existing_workspace_row(
connection,
requested_workspace_id,
workspace_paths,
)?
.ok_or_else(|| DomainError::Storage {
message: format!(
"Failed to register workspace {requested_workspace_id}: row missing after upsert"
),
repair: Some("ee doctor".to_owned()),
})?
.id)
}
pub(crate) fn bound_workspace_id_from_path(workspace_path: &Path) -> String {
let canonical = workspace_path
.canonicalize()
.unwrap_or_else(|_| workspace_path.to_path_buf());
let requested = stable_workspace_id(&canonical);
let database_path = workspace_path.join(".ee").join("ee.db");
if !database_path.exists() {
return requested;
}
let Ok(connection) = DbConnection::open_file_read_only(&database_path) else {
return requested;
};
bound_workspace_id_or_hash(
&connection,
&requested,
&[workspace_path, canonical.as_path()],
)
.unwrap_or(requested)
}
pub(crate) fn bound_workspace_id_or_hash(
connection: &DbConnection,
requested_workspace_id: &str,
workspace_paths: &[&Path],
) -> Result<String, DomainError> {
Ok(
select_existing_workspace_row(connection, requested_workspace_id, workspace_paths)?
.map_or_else(|| requested_workspace_id.to_owned(), |row| row.id),
)
}
pub(crate) fn select_existing_workspace_row(
connection: &DbConnection,
requested_workspace_id: &str,
workspace_paths: &[&Path],
) -> Result<Option<StoredWorkspace>, DomainError> {
let mut matches = BTreeMap::new();
let mut input_keys = BTreeSet::new();
for workspace_path in workspace_paths {
for key in workspace_path_lookup_keys(workspace_path) {
input_keys.insert(key.clone());
if let Some(existing) =
connection
.get_workspace_by_path(&key)
.map_err(|error| DomainError::Storage {
message: format!("Failed to query workspace: {error}"),
repair: Some("ee doctor".to_owned()),
})?
{
matches.entry(existing.id.clone()).or_insert(existing);
}
}
}
if matches.is_empty() && !input_keys.is_empty() {
for row in connection
.list_workspaces()
.map_err(|error| DomainError::Storage {
message: format!("Failed to list workspaces: {error}"),
repair: Some("ee doctor".to_owned()),
})?
{
if workspace_path_lookup_keys(Path::new(&row.path))
.iter()
.any(|key| input_keys.contains(key))
{
matches.entry(row.id.clone()).or_insert(row);
}
}
}
if !matches.is_empty() {
return Ok(Some(pick_workspace_row(
connection,
matches.into_values().collect(),
)?));
}
connection
.get_workspace(requested_workspace_id)
.map_err(|error| DomainError::Storage {
message: format!("Failed to query workspace by id: {error}"),
repair: Some("ee doctor".to_owned()),
})
}
pub(crate) fn pick_workspace_row(
connection: &DbConnection,
rows: Vec<StoredWorkspace>,
) -> Result<StoredWorkspace, DomainError> {
let mut best: Option<(u64, String, StoredWorkspace)> = None;
for row in rows {
let live = connection
.count_live_memories_for_workspace(&row.id)
.map_err(|error| DomainError::Storage {
message: format!("Failed to count live memories for workspace: {error}"),
repair: Some("ee doctor".to_owned()),
})?;
let better = match &best {
None => true,
Some((best_live, best_id, _)) => {
live > *best_live || (live == *best_live && row.id < *best_id)
}
};
if better {
best = Some((live, row.id.clone(), row));
}
}
best.ok_or_else(|| DomainError::Storage {
message: "workspace row picker received an empty match set".to_owned(),
repair: Some("ee doctor".to_owned()),
})
.map(|(_, _, row)| row)
}
fn workspace_path_lookup_keys(path: &Path) -> Vec<String> {
let mut seen = BTreeSet::new();
let mut keys = Vec::new();
let mut push = |value: String| {
let trimmed = trim_trailing_path_separators(&value);
if seen.insert(value.clone()) {
keys.push(value);
}
if seen.insert(trimmed.clone()) {
keys.push(trimmed);
}
};
let raw = path.to_string_lossy().into_owned();
push(raw.clone());
push(strip_windows_verbatim_prefix(&raw));
push(normalize_lexical(path).to_string_lossy().into_owned());
if let Ok(canonical) = path.canonicalize() {
let rendered = canonical.to_string_lossy().into_owned();
push(rendered.clone());
push(strip_windows_verbatim_prefix(&rendered));
}
keys
}
fn trim_trailing_path_separators(value: &str) -> String {
let mut trimmed = value.to_owned();
while trimmed.len() > 1 {
let Some(last) = trimmed.as_bytes().last().copied() else {
break;
};
if last != b'/' && last != b'\\' {
break;
}
if trimmed.len() == 3 && trimmed.as_bytes()[1] == b':' {
break;
}
trimmed.pop();
}
trimmed
}
#[allow(dead_code, reason = "N4.3 staged token-threaded workspace ID helper")]
pub(crate) fn stable_workspace_id_seeded(
path: &Path,
determinism: &mut Deterministic<Seed>,
) -> String {
let workspace_scope = determinism.child("ulid.workspace");
let seed_material = format!(
"{}:{}",
workspace_scope.seed().as_u64(),
path.to_string_lossy()
);
let mut path_token = Deterministic::from_persistent_seed(seed_material.as_bytes());
WorkspaceId::from_uuid(path_token.clock().next_uuid_v7()).to_string()
}
#[cfg(test)]
mod tests {
use std::fs;
use std::thread;
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
use crate::core::hygiene_coordination::{ActiveAgent, AgentMailReservation};
use crate::core::swarm_brief::{
WorkspaceGitOperationState, WorkspaceGitPathMetadata, WorkspaceGitStatusEntry,
agent_mail_snapshot_project_key_for_workspace,
};
type TestResult = Result<(), String>;
fn unique_dir(prefix: &str) -> Result<PathBuf, String> {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_nanos();
Ok(env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id())))
}
fn initialized_workspace(prefix: &str) -> Result<PathBuf, String> {
let root = unique_dir(prefix)?;
fs::create_dir_all(root.join(WORKSPACE_MARKER)).map_err(|error| error.to_string())?;
Ok(root)
}
fn declared_agent_mail_snapshot(workspace: &Path) -> Result<Value, String> {
let schema: Value = serde_json::from_str(include_str!(
"../../docs/schemas/swarm/ee.agent_mail.snapshot.v1.json"
))
.map_err(|error| error.to_string())?;
let mut snapshot = schema
.pointer("/examples/0")
.cloned()
.ok_or_else(|| "Agent Mail schema example is missing".to_owned())?;
snapshot["generated_at"] = serde_json::json!(Utc::now().to_rfc3339());
snapshot["project_key"] =
serde_json::json!(agent_mail_snapshot_project_key_for_workspace(workspace)?);
Ok(snapshot)
}
fn status_entry(path: &str, staged: &str, unstaged: &str) -> WorkspaceGitStatusEntry {
WorkspaceGitStatusEntry {
path: path.to_owned(),
original_path: None,
staged: staged.to_owned(),
unstaged: unstaged.to_owned(),
entry_kind: "ordinary".to_owned(),
submodule_state: None,
metadata: None,
}
}
fn untracked_status_entry(path: &str) -> WorkspaceGitStatusEntry {
WorkspaceGitStatusEntry {
path: path.to_owned(),
original_path: None,
staged: "?".to_owned(),
unstaged: "?".to_owned(),
entry_kind: "untracked".to_owned(),
submodule_state: None,
metadata: None,
}
}
fn file_status_entry(
path: &str,
staged: &str,
unstaged: &str,
size_bytes: u64,
) -> WorkspaceGitStatusEntry {
let mut entry = status_entry(path, staged, unstaged);
entry.metadata = Some(WorkspaceGitPathMetadata {
exists: true,
file_type: "file".to_owned(),
size_bytes: Some(size_bytes),
large_file: false,
skip_reason: None,
});
entry
}
fn file_untracked_status_entry(path: &str, size_bytes: u64) -> WorkspaceGitStatusEntry {
let mut entry = untracked_status_entry(path);
entry.metadata = Some(WorkspaceGitPathMetadata {
exists: true,
file_type: "file".to_owned(),
size_bytes: Some(size_bytes),
large_file: false,
skip_reason: None,
});
entry
}
fn hygiene_snapshot(entries: Vec<WorkspaceGitStatusEntry>) -> WorkspaceGitSnapshot {
WorkspaceGitSnapshot {
repository_root: "/repo".to_owned(),
entries,
operation_state: WorkspaceGitOperationState::default(),
}
}
fn hygiene_report_from_parts(
snapshot: WorkspaceGitSnapshot,
agent_mail_input: &AgentMailCoordinationInput,
beads_metadata_signal: BeadsMetadataSignal,
beads_reservations: &[BeadsReservationHolder],
) -> WorkspaceHygieneReport {
hygiene_report_from_parts_with_jsonl(
snapshot,
agent_mail_input,
beads_metadata_signal,
beads_reservations,
Some(b"{\"id\":\"bd-test\",\"title\":\"test\"}\n"),
)
}
fn hygiene_report_from_parts_with_jsonl(
snapshot: WorkspaceGitSnapshot,
agent_mail_input: &AgentMailCoordinationInput,
beads_metadata_signal: BeadsMetadataSignal,
beads_reservations: &[BeadsReservationHolder],
jsonl_content: Option<&[u8]>,
) -> WorkspaceHygieneReport {
build_workspace_hygiene_report_from_inputs(WorkspaceHygieneReportInputs {
workspace_path: Path::new("/repo"),
snapshot,
classifier_config: &HygieneClassifierConfig::default(),
jsonl_content,
self_agent_name: Some("IvoryCondor"),
beads_metadata_signal,
beads_reservations,
agent_mail_input,
now: DateTime::parse_from_rfc3339("2026-05-18T08:00:00Z")
.expect("valid test timestamp")
.with_timezone(&Utc),
})
}
fn hygiene_report_from_workspace_parts(
workspace_path: &Path,
snapshot: WorkspaceGitSnapshot,
agent_mail_input: &AgentMailCoordinationInput,
beads_metadata_signal: BeadsMetadataSignal,
beads_reservations: &[BeadsReservationHolder],
) -> WorkspaceHygieneReport {
build_workspace_hygiene_report_from_inputs(WorkspaceHygieneReportInputs {
workspace_path,
snapshot,
classifier_config: &HygieneClassifierConfig::default(),
jsonl_content: Some(b"{\"id\":\"bd-test\",\"title\":\"test\"}\n"),
self_agent_name: Some("IvoryCondor"),
beads_metadata_signal,
beads_reservations,
agent_mail_input,
now: DateTime::parse_from_rfc3339("2026-05-18T08:00:00Z")
.expect("valid test timestamp")
.with_timezone(&Utc),
})
}
fn write_file(path: &Path, body: &str) -> TestResult {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
}
fs::write(path, body).map_err(|error| error.to_string())
}
#[test]
fn detects_beads_db_pending_flush_when_db_marker_is_newer_than_jsonl() -> TestResult {
let workspace = unique_dir("ee-beads-db-newer")?;
let beads_dir = workspace.join(".beads");
write_file(&beads_dir.join("issues.jsonl"), "{\"id\":\"bd-test\"}\n")?;
thread::sleep(Duration::from_millis(1_100));
write_file(&beads_dir.join("beads.db"), "sqlite marker")?;
assert_eq!(
detect_beads_metadata_signal(&workspace),
BeadsMetadataSignal::DbDirtyPendingFlush
);
Ok(())
}
#[test]
fn detects_beads_external_import_pending_when_jsonl_is_newer_than_db_marker() -> TestResult {
let workspace = unique_dir("ee-beads-jsonl-newer")?;
let beads_dir = workspace.join(".beads");
write_file(&beads_dir.join("beads.db"), "sqlite marker")?;
thread::sleep(Duration::from_millis(1_100));
write_file(&beads_dir.join("issues.jsonl"), "{\"id\":\"bd-test\"}\n")?;
assert_eq!(
detect_beads_metadata_signal(&workspace),
BeadsMetadataSignal::ExternalChangesPendingImport
);
Ok(())
}
#[test]
fn loads_agent_mail_snapshot_as_coordination_input() -> TestResult {
let workspace = unique_dir("ee-agent-mail-snapshot")?;
fs::create_dir_all(&workspace).map_err(|error| error.to_string())?;
let snapshot_path = workspace.join("agent-mail.json");
let mut snapshot = declared_agent_mail_snapshot(&workspace)?;
snapshot["file_reservations"][0]["path_pattern"] =
serde_json::json!("src/core/workspace.rs");
snapshot["file_reservations"][0]["holder"] = serde_json::json!("OtherAgent");
write_file(&snapshot_path, &snapshot.to_string())?;
let input =
load_agent_mail_coordination_input(Some(&snapshot_path), &workspace, Utc::now());
let AgentMailCoordinationInput::Available {
reservations,
active_agents,
} = input
else {
return Err("snapshot must load as available Agent Mail input".to_string());
};
assert_eq!(reservations.len(), 1);
assert_eq!(reservations[0].path_pattern, "src/core/workspace.rs");
assert_eq!(reservations[0].holder_agent, "OtherAgent");
assert_eq!(active_agents.len(), 1);
assert_eq!(active_agents[0].name, "BeigeHollow");
Ok(())
}
#[test]
fn agent_mail_snapshot_authority_rejects_stale_or_other_workspace() -> TestResult {
let workspace = unique_dir("ee-agent-mail-snapshot-authority")?;
let other_workspace = unique_dir("ee-agent-mail-snapshot-other-workspace")?;
fs::create_dir_all(&workspace).map_err(|error| error.to_string())?;
fs::create_dir_all(&other_workspace).map_err(|error| error.to_string())?;
let cases = [
("stale", "2000-01-01T00:00:00Z".to_owned(), None),
(
"other-workspace",
Utc::now().to_rfc3339(),
Some(agent_mail_snapshot_project_key_for_workspace(
&other_workspace,
)?),
),
];
for (name, generated_at, project_key) in cases {
let mut snapshot = declared_agent_mail_snapshot(&workspace)?;
snapshot["generated_at"] = serde_json::json!(generated_at);
if let Some(project_key) = project_key {
snapshot["project_key"] = serde_json::json!(project_key);
}
let path = workspace.join(format!("{name}.json"));
write_file(&path, &snapshot.to_string())?;
let input = load_agent_mail_coordination_input(Some(&path), &workspace, Utc::now());
assert!(
matches!(input, AgentMailCoordinationInput::Unavailable),
"{name} declared-v1 evidence must be unavailable"
);
}
let mut corrupt = declared_agent_mail_snapshot(&workspace)?;
corrupt["producer_status"] = serde_json::json!("degraded");
corrupt["fallback_active"] = serde_json::json!(true);
corrupt["durability_state"] = serde_json::json!("corrupt");
corrupt["recovery"] = serde_json::json!({
"mode": "corrupt",
"reason": "archive_corruption"
});
let corrupt_path = workspace.join("corrupt.json");
write_file(&corrupt_path, &corrupt.to_string())?;
let corrupt_input =
load_agent_mail_coordination_input(Some(&corrupt_path), &workspace, Utc::now());
assert!(
matches!(corrupt_input, AgentMailCoordinationInput::Unavailable),
"recovery-corrupt declared-v1 evidence must be unavailable"
);
Ok(())
}
#[test]
fn agent_mail_snapshot_read_refuses_oversized_file() -> TestResult {
let workspace = unique_dir("ee-agent-mail-snapshot-oversize")?;
let snapshot_path = workspace.join("agent-mail-oversize.json");
if let Some(parent) = snapshot_path.parent() {
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
}
let payload = vec![b' '; AGENT_MAIL_SNAPSHOT_MAX_BYTES + 1];
fs::write(&snapshot_path, &payload).map_err(|error| error.to_string())?;
let direct = read_agent_mail_snapshot(&snapshot_path)
.expect_err("oversized snapshot must be refused before allocation");
assert_eq!(direct.kind(), io::ErrorKind::InvalidData);
let message = direct.to_string();
assert!(
message.contains(&AGENT_MAIL_SNAPSHOT_MAX_BYTES.to_string()),
"rejection must cite the cap; got {message:?}",
);
let input =
load_agent_mail_coordination_input(Some(&snapshot_path), &workspace, Utc::now());
assert!(
matches!(input, AgentMailCoordinationInput::Unavailable),
"oversized snapshot must surface as Unavailable at the coordination layer; got {input:?}",
);
Ok(())
}
#[cfg(unix)]
#[test]
fn agent_mail_snapshot_final_open_rejects_symlink_leaf() -> TestResult {
use std::os::unix::fs::symlink;
let workspace = unique_dir("ee-agent-mail-snapshot-symlink")?;
let outside = unique_dir("ee-agent-mail-snapshot-symlink-target")?;
fs::create_dir_all(&workspace).map_err(|error| error.to_string())?;
fs::create_dir_all(&outside).map_err(|error| error.to_string())?;
let outside_snapshot = outside.join("snapshot.json");
let linked_snapshot = workspace.join("snapshot.json");
fs::write(&outside_snapshot, r#"{"file_reservations":[]}"#)
.map_err(|error| error.to_string())?;
symlink(&outside_snapshot, &linked_snapshot).map_err(|error| error.to_string())?;
let result = open_agent_mail_snapshot_for_read_no_follow(&linked_snapshot);
assert!(
result.is_err(),
"final Agent Mail snapshot open must reject a symlink leaf"
);
assert_eq!(
fs::read_to_string(&outside_snapshot).map_err(|error| error.to_string())?,
r#"{"file_reservations":[]}"#,
"symlink target should remain untouched"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn workspace_bounded_file_reader_rejects_symlink_leaf() -> TestResult {
use std::os::unix::fs::symlink;
let workspace = unique_dir("ee-workspace-bounded-reader-symlink")?;
let outside = unique_dir("ee-workspace-bounded-reader-target")?;
fs::create_dir_all(&workspace).map_err(|error| error.to_string())?;
fs::create_dir_all(&outside).map_err(|error| error.to_string())?;
let outside_file = outside.join("payload.txt");
let linked_file = workspace.join("payload.txt");
fs::write(&outside_file, "outside payload").map_err(|error| error.to_string())?;
symlink(&outside_file, &linked_file).map_err(|error| error.to_string())?;
let result = read_bounded_file(&linked_file, 64);
assert!(
result.is_err(),
"workspace bounded reader must reject a symlink leaf"
);
assert_eq!(
fs::read_to_string(&outside_file).map_err(|error| error.to_string())?,
"outside payload",
"symlink target should remain untouched"
);
Ok(())
}
#[test]
fn alias_validation_rejects_paths() {
assert!(normalize_alias("client-api").is_ok());
assert!(normalize_alias("client.api").is_ok());
assert!(normalize_alias("client/api").is_err());
assert!(normalize_alias(".").is_err());
assert!(normalize_alias("..").is_err());
assert!(normalize_alias("...").is_err());
assert!(normalize_alias("..foo").is_err());
assert!(normalize_alias(".bar.").is_err());
assert!(normalize_alias("foo..").is_ok());
}
#[test]
fn stable_workspace_id_ignores_windows_verbatim_prefix() {
let drive = Path::new(r"C:\Users\dev\ee-tc-win-soak6");
let verbatim = Path::new(r"\\?\C:\Users\dev\ee-tc-win-soak6");
assert_eq!(stable_workspace_id(drive), stable_workspace_id(verbatim));
assert!(stable_workspace_id(drive).starts_with("wsp_"));
}
#[test]
fn workspace_path_lookup_keys_include_lexical_and_trailing_slash_forms() {
let lexical = Path::new("/tmp/ee-lookup/./campaign");
let keys = workspace_path_lookup_keys(lexical);
assert!(
keys.iter()
.any(|key| key.ends_with("/campaign") && !key.contains("/./")),
"lookup keys should include the lexically normalized path, got {keys:?}"
);
let slashed = workspace_path_lookup_keys(Path::new("/tmp/ee-lookup/campaign/"));
assert!(
slashed.iter().any(|key| key == "/tmp/ee-lookup/campaign"),
"lookup keys should include the trailing-slash-trimmed path, got {slashed:?}"
);
}
#[test]
fn ensure_bound_workspace_reuses_lexical_row_when_caller_is_canonical() -> TestResult {
let root = unique_dir("ee-bound-workspace")?;
fs::create_dir_all(&root).map_err(|error| error.to_string())?;
let canonical = root.canonicalize().map_err(|error| error.to_string())?;
let Some(name) = canonical.file_name() else {
return Err("canonical workspace path missing file name".to_owned());
};
let lexical = canonical.join("..").join(name);
assert_ne!(
lexical, canonical,
"lexical alias must differ from the canonical path"
);
let connection = DbConnection::open_memory().map_err(|error| error.to_string())?;
connection.migrate().map_err(|error| error.to_string())?;
let occupied_id = "wsp_00000000000000000000legacy";
connection
.insert_workspace(
occupied_id,
&CreateWorkspaceInput {
path: lexical.to_string_lossy().into_owned(),
name: Some("legacy lexical workspace".to_owned()),
},
)
.map_err(|error| error.to_string())?;
let requested = stable_workspace_id(&canonical);
assert_ne!(
requested, occupied_id,
"hashed id must differ from the stored path-keyed id"
);
let bound = ensure_bound_workspace(&connection, &requested, &[&canonical])
.map_err(|error| error.message())?;
assert_eq!(bound, occupied_id, "bind to stored path row");
let workspaces = connection
.list_workspaces()
.map_err(|error| error.to_string())?;
assert_eq!(
workspaces.len(),
1,
"must not invent a second workspace row"
);
Ok(())
}
#[test]
fn pick_workspace_row_prefers_the_occupied_id() -> TestResult {
let connection = DbConnection::open_memory().map_err(|error| error.to_string())?;
connection.migrate().map_err(|error| error.to_string())?;
let empty = StoredWorkspace {
id: "wsp_bbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(),
path: "/tmp/ee-empty".to_owned(),
name: Some("empty".to_owned()),
scope_kind: "standalone".to_owned(),
repository_root: None,
repository_fingerprint: None,
subproject_path: None,
created_at: "2026-01-01T00:00:00Z".to_owned(),
updated_at: "2026-01-01T00:00:00Z".to_owned(),
};
let occupied = StoredWorkspace {
id: "wsp_aaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(),
path: "/tmp/ee-occupied".to_owned(),
name: Some("occupied".to_owned()),
scope_kind: "standalone".to_owned(),
repository_root: None,
repository_fingerprint: None,
subproject_path: None,
created_at: "2026-01-01T00:00:00Z".to_owned(),
updated_at: "2026-01-01T00:00:00Z".to_owned(),
};
connection
.insert_workspace(
&occupied.id,
&CreateWorkspaceInput {
path: occupied.path.clone(),
name: occupied.name.clone(),
},
)
.map_err(|error| error.to_string())?;
connection
.insert_workspace(
&empty.id,
&CreateWorkspaceInput {
path: empty.path.clone(),
name: empty.name.clone(),
},
)
.map_err(|error| error.to_string())?;
connection
.insert_memory(
"mem_00000000000000000000000001",
&crate::db::CreateMemoryInput {
workspace_id: occupied.id.clone(),
level: "procedural".to_owned(),
kind: "rule".to_owned(),
content: "occupied workspace memory".to_owned(),
workflow_id: None,
confidence: 0.9,
utility: 0.5,
importance: 0.5,
provenance_uri: None,
trust_class: crate::models::TrustClass::HumanExplicit.as_str().to_owned(),
trust_subclass: None,
tags: Vec::new(),
valid_from: None,
valid_to: None,
},
)
.map_err(|error| error.to_string())?;
let picked = pick_workspace_row(&connection, vec![empty, occupied.clone()])
.map_err(|error| error.message())?;
assert_eq!(picked.id, occupied.id, "occupied workspace wins");
Ok(())
}
#[test]
fn stable_workspace_id_seeded_replays_with_same_seed_and_path() {
let path = Path::new("/tmp/ee-seeded-workspace");
let mut first_token = Deterministic::from_seed(44);
let first = stable_workspace_id_seeded(path, &mut first_token);
let second = stable_workspace_id_seeded(path, &mut first_token);
let mut replay_token = Deterministic::from_seed(44);
assert_eq!(first, stable_workspace_id_seeded(path, &mut replay_token));
assert_eq!(second, stable_workspace_id_seeded(path, &mut replay_token));
let mut other_seed = Deterministic::from_seed(45);
assert_ne!(first, stable_workspace_id_seeded(path, &mut other_seed));
let mut other_path = Deterministic::from_seed(44);
assert_ne!(
first,
stable_workspace_id_seeded(Path::new("/tmp/ee-other-workspace"), &mut other_path)
);
assert!(first.starts_with("wsp_"));
}
#[test]
fn workspace_row_for_uninitialized_path_quotes_init_repair() -> TestResult {
let workspace = unique_dir("ee workspace needs-init's")?;
fs::create_dir_all(&workspace).map_err(|error| error.to_string())?;
let error = workspace_row_for_path(&workspace.display().to_string())
.expect_err("workspace without .ee marker must fail");
assert_eq!(
error.repair().as_deref(),
Some(format!(
"ee init --workspace '{}'",
workspace.display().to_string().replace('\'', "'\\''")
))
.as_deref()
);
Ok(())
}
#[test]
fn resolve_reports_local_migration_without_mutating_the_store() -> TestResult {
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let workspace = temp.path().join("workspace with 'quotes'");
let marker = workspace.join(WORKSPACE_MARKER);
fs::create_dir_all(&marker).map_err(|error| error.to_string())?;
let workspace = canonical_or_lexical(&workspace);
let database = marker.join("ee.db");
let db = DbConnection::open_file(&database).map_err(|error| error.to_string())?;
db.close().map_err(|error| error.to_string())?;
let before = fs::read(&database).map_err(|error| error.to_string())?;
let registry = temp.path().join("absent-registry.db");
let error = resolve_workspace_report(&WorkspaceResolveOptions {
workspace_path: Some(workspace.clone()),
target: None,
registry_path: Some(registry.clone()),
})
.expect_err("unmigrated local store must request migration");
let DomainError::MigrationRequired { message, repair } = error else {
return Err(format!("unexpected error: {}", error.message()));
};
assert!(message.contains("Workspace database requires migration"));
assert_eq!(
repair,
Some(format!(
"ee migrate run --workspace {}",
shell_quote_path_arg(&workspace)
))
);
assert_eq!(
fs::read(&database).map_err(|error| error.to_string())?,
before
);
assert!(!registry.exists());
Ok(())
}
#[test]
fn binding_persists_detected_scope_and_keeps_existing_metadata() -> TestResult {
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let repository = temp.path().join("repository");
fs::create_dir_all(&repository).map_err(|error| error.to_string())?;
let git = std::process::Command::new("git")
.args(["init", "--quiet", "--initial-branch=main"])
.current_dir(&repository)
.output()
.map_err(|error| error.to_string())?;
assert!(
git.status.success(),
"{}",
String::from_utf8_lossy(&git.stderr)
);
let repository = canonical_or_lexical(&repository);
for (path, kind, subproject) in [
(temp.path().join("standalone"), "standalone", None),
(repository.clone(), "repository", None),
(
repository.join("services/api"),
"subproject",
Some(Path::new("services/api")),
),
] {
fs::create_dir_all(path.join(WORKSPACE_MARKER)).map_err(|error| error.to_string())?;
let path = canonical_or_lexical(&path);
let db = DbConnection::open_file(path.join(WORKSPACE_MARKER).join("ee.db"))
.map_err(|error| error.to_string())?;
db.migrate().map_err(|error| error.to_string())?;
let id = stable_workspace_id(&path);
assert_eq!(
ensure_bound_workspace(&db, &id, &[&path]).map_err(|error| error.message())?,
id
);
db.update_workspace_name(&id, Some("preserved-name"))
.map_err(|error| error.to_string())?;
let before = db
.get_workspace(&id)
.map_err(|error| error.to_string())?
.ok_or("bound workspace missing")?;
assert_eq!(before.scope_kind, kind);
assert_eq!(before.subproject_path.as_deref().map(Path::new), subproject);
if kind == "standalone" {
assert!(before.repository_root.is_none());
assert!(before.repository_fingerprint.is_none());
} else {
assert_eq!(before.repository_root.as_deref(), repository.to_str());
assert!(
before
.repository_fingerprint
.as_deref()
.is_some_and(|value| value.starts_with("repo:"))
);
}
let unrelated_id = stable_workspace_id(&temp.path().join("unrelated"));
assert_eq!(
ensure_bound_workspace(&db, &unrelated_id, &[&path])
.map_err(|error| error.message())?,
id
);
assert_eq!(
db.get_workspace(&id).map_err(|error| error.to_string())?,
Some(before.clone())
);
db.close().map_err(|error| error.to_string())?;
let resolved = resolve_workspace_report(&WorkspaceResolveOptions {
workspace_path: Some(path),
target: None,
registry_path: Some(temp.path().join("absent-registry.db")),
})
.map_err(|error| error.message())?;
assert_eq!(resolved.workspace_id, id);
assert_eq!(resolved.scope_kind, kind);
assert_eq!(resolved.repository_root, before.repository_root);
assert_eq!(
resolved.repository_fingerprint,
before.repository_fingerprint
);
assert_eq!(resolved.subproject_path, before.subproject_path);
}
assert!(!temp.path().join("absent-registry.db").exists());
Ok(())
}
#[test]
fn alias_command_registers_and_resolves_workspace() -> TestResult {
let workspace = initialized_workspace("ee-workspace-alias")?;
let registry = unique_dir("ee-workspace-registry")?.join("registry.db");
let report = alias_workspace(&WorkspaceAliasOptions {
workspace_path: Some(workspace.clone()),
pick: None,
alias: Some("client-api".to_string()),
clear: false,
dry_run: false,
registry_path: Some(registry.clone()),
})
.map_err(|error| error.message())?;
assert!(report.persisted);
assert_eq!(report.alias.as_deref(), Some("client-api"));
let resolved = resolve_workspace_report(&WorkspaceResolveOptions {
workspace_path: None,
target: Some("client-api".to_string()),
registry_path: Some(registry),
})
.map_err(|error| error.message())?;
assert_eq!(resolved.source, "alias");
assert_eq!(
PathBuf::from(resolved.root),
workspace.canonicalize().unwrap_or(workspace)
);
Ok(())
}
#[test]
fn alias_dry_run_does_not_create_registry() -> TestResult {
let workspace = initialized_workspace("ee-workspace-alias-dry")?;
let registry = unique_dir("ee-workspace-registry-dry")?.join("registry.db");
let report = alias_workspace(&WorkspaceAliasOptions {
workspace_path: Some(workspace),
pick: None,
alias: Some("dry-run".to_string()),
clear: false,
dry_run: true,
registry_path: Some(registry.clone()),
})
.map_err(|error| error.message())?;
assert!(!report.persisted);
assert!(!registry.exists());
Ok(())
}
#[test]
fn registry_list_reports_missing_registry_without_creating_it() -> TestResult {
let registry = unique_dir("ee-workspace-registry-missing")?.join("registry.db");
let report = list_workspace_registry(&WorkspaceListOptions {
registry_path: Some(registry.clone()),
})
.map_err(|error| error.message())?;
assert!(!report.registry_exists);
assert!(!registry.exists());
assert!(report.workspaces.is_empty());
Ok(())
}
#[test]
fn registry_list_accepts_canonical_absolute_registry_path() -> TestResult {
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let canonical_parent =
fs::canonicalize(temp.path()).unwrap_or_else(|_| temp.path().to_path_buf());
let registry = canonical_parent.join("registry.db");
let report = list_workspace_registry(&WorkspaceListOptions {
registry_path: Some(registry.clone()),
})
.map_err(|error| error.message())?;
assert_eq!(report.registry_path, registry.display().to_string());
assert!(!report.registry_exists);
assert!(!registry.exists());
Ok(())
}
#[test]
fn registry_list_rejects_directory_registry_path() -> TestResult {
let registry = unique_dir("ee-workspace-registry-dir")?.join("registry.db");
fs::create_dir_all(®istry).map_err(|error| error.to_string())?;
let error = match list_workspace_registry(&WorkspaceListOptions {
registry_path: Some(registry),
}) {
Ok(_) => return Err("directory registry path must be rejected".to_string()),
Err(error) => error,
};
assert!(
error.message().contains("not a regular file"),
"unexpected error: {}",
error.message()
);
Ok(())
}
#[test]
fn registry_write_rejects_directory_registry_path() -> TestResult {
let registry = unique_dir("ee-workspace-registry-write-dir")?.join("registry.db");
fs::create_dir_all(®istry).map_err(|error| error.to_string())?;
let error = match open_registry_write(®istry) {
Ok(_) => return Err("directory registry path must be rejected".to_string()),
Err(error) => error,
};
assert!(
error.message().contains("not a regular file"),
"unexpected error: {}",
error.message()
);
Ok(())
}
#[cfg(unix)]
#[test]
fn registry_write_rejects_symlinked_parent() -> TestResult {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let outside_parent = temp.path().join("outside-registry-parent");
fs::create_dir_all(&outside_parent).map_err(|error| error.to_string())?;
let linked_parent = temp.path().join("linked-registry-parent");
symlink(&outside_parent, &linked_parent).map_err(|error| error.to_string())?;
let registry = linked_parent.join("registry.db");
let error = match open_registry_write(®istry) {
Ok(_) => return Err("symlinked parent must be rejected".to_string()),
Err(error) => error,
};
assert!(
error.message().contains("symlink component"),
"unexpected error: {}",
error.message()
);
assert!(
!outside_parent.join("registry.db").exists(),
"registry write must not follow a symlinked parent"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn registry_read_rejects_symlinked_registry_file() -> TestResult {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let outside_registry = temp.path().join("outside-registry.db");
fs::write(&outside_registry, b"not a trusted registry")
.map_err(|error| error.to_string())?;
let registry = temp.path().join("registry.db");
symlink(&outside_registry, ®istry).map_err(|error| error.to_string())?;
let error = match open_registry_read_only(®istry) {
Ok(_) => return Err("symlinked registry must be rejected".to_string()),
Err(error) => error,
};
assert!(
error.message().contains("symlink component"),
"unexpected error: {}",
error.message()
);
Ok(())
}
#[cfg(unix)]
#[test]
fn registry_list_rejects_dangling_symlink_registry_file() -> TestResult {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir().map_err(|error| error.to_string())?;
let registry = temp.path().join("registry.db");
symlink(temp.path().join("missing-outside-registry.db"), ®istry)
.map_err(|error| error.to_string())?;
let error = match list_workspace_registry(&WorkspaceListOptions {
registry_path: Some(registry),
}) {
Ok(_) => return Err("dangling symlinked registry must be rejected".to_string()),
Err(error) => error,
};
assert!(
error.message().contains("symlink component"),
"unexpected error: {}",
error.message()
);
Ok(())
}
#[test]
fn hygiene_report_applies_precollected_agent_mail_reservations() {
let agent_mail = AgentMailCoordinationInput::Available {
reservations: vec![AgentMailReservation {
path_pattern: "src/core/workspace.rs".to_owned(),
holder_agent: "OtherAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T09:00:00Z".to_owned()),
reservation_id: Some("reservation-1".to_owned()),
bead_id: Some("bd-1eq3l.5".to_owned()),
thread_id: Some("bd-1eq3l.5".to_owned()),
}],
active_agents: vec![ActiveAgent {
name: "OtherAgent".to_owned(),
last_active_at: Some("2026-05-18T07:55:00Z".to_owned()),
}],
};
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![status_entry("src/core/workspace.rs", ".", "M")]),
&agent_mail,
BeadsMetadataSignal::Unknown,
&[],
);
assert!(report.coordination.agent_mail_available);
assert_eq!(report.coordination.active_agent_count, 1);
assert_eq!(report.coordination.blocked_by_coordination.len(), 1);
assert_eq!(
report.coordination.blocked_by_coordination[0].path,
"src/core/workspace.rs"
);
assert!(
report.staging_groups.iter().all(|group| group
.paths
.iter()
.all(|path| path != "src/core/workspace.rs")),
"coordination-blocked paths must not be suggested as commit-ready"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_AGENT_MAIL_UNAVAILABLE_CODE),
"available Agent Mail input must not report unavailable degradation"
);
}
#[test]
fn hygiene_report_applies_beads_metadata_signal_and_reservation_priority() {
let beads_reservations = vec![BeadsReservationHolder {
agent_name: "OtherAgent".to_owned(),
exclusive: true,
expires_ts_rfc3339: "2026-05-18T09:00:00Z".to_owned(),
}];
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![status_entry(BEADS_JSONL_RELATIVE_PATH, ".", "M")]),
&AgentMailCoordinationInput::Unavailable,
BeadsMetadataSignal::DbDirtyPendingFlush,
&beads_reservations,
);
assert_eq!(
report.beads_state.classification,
crate::core::hygiene_beads_state::BeadsClassification::BeadsReservedByOtherAgent
);
assert_eq!(
report.beads_state.metadata_signal,
BeadsMetadataSignal::DbDirtyPendingFlush
);
assert_eq!(report.beads_state.reservation_holders.len(), 1);
}
#[test]
fn hygiene_report_derives_beads_reservation_overlay_from_agent_mail_snapshot() {
let now = DateTime::parse_from_rfc3339("2026-05-18T08:00:00Z")
.expect("valid test timestamp")
.with_timezone(&Utc);
let agent_mail = AgentMailCoordinationInput::Available {
reservations: vec![
AgentMailReservation {
path_pattern: BEADS_JSONL_RELATIVE_PATH.to_owned(),
holder_agent: "OtherAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T09:00:00Z".to_owned()),
reservation_id: Some("beads-reservation".to_owned()),
bead_id: None,
thread_id: None,
},
AgentMailReservation {
path_pattern: "src/core/workspace.rs".to_owned(),
holder_agent: "SourceAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T09:00:00Z".to_owned()),
reservation_id: Some("source-reservation".to_owned()),
bead_id: None,
thread_id: None,
},
AgentMailReservation {
path_pattern: BEADS_JSONL_RELATIVE_PATH.to_owned(),
holder_agent: "ExpiredAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T07:59:59Z".to_owned()),
reservation_id: Some("expired-beads-reservation".to_owned()),
bead_id: None,
thread_id: None,
},
],
active_agents: vec![ActiveAgent {
name: "OtherAgent".to_owned(),
last_active_at: Some("2026-05-18T07:55:00Z".to_owned()),
}],
};
let beads_reservations = beads_reservations_from_agent_mail_input(&agent_mail, now);
let report = build_workspace_hygiene_report_from_inputs(WorkspaceHygieneReportInputs {
workspace_path: Path::new("/repo"),
snapshot: hygiene_snapshot(vec![status_entry(BEADS_JSONL_RELATIVE_PATH, ".", "M")]),
classifier_config: &HygieneClassifierConfig::default(),
jsonl_content: Some(b"{\"id\":\"bd-test\",\"title\":\"test\"}\n"),
self_agent_name: Some("IvoryCondor"),
beads_metadata_signal: BeadsMetadataSignal::Unknown,
beads_reservations: &beads_reservations,
agent_mail_input: &agent_mail,
now,
});
assert_eq!(beads_reservations.len(), 1);
assert_eq!(beads_reservations[0].agent_name, "OtherAgent");
assert_eq!(
report.beads_state.classification,
crate::core::hygiene_beads_state::BeadsClassification::BeadsReservedByOtherAgent
);
assert_eq!(
report.beads_state.reservation_holders[0].agent_name,
"OtherAgent"
);
assert!(
report
.coordination
.blocked_by_coordination
.iter()
.any(|blocked| blocked.path == BEADS_JSONL_RELATIVE_PATH),
"general coordination overlay should also block the dirty Beads path"
);
}
#[test]
fn hygiene_recommendations_split_logical_groups_and_explain_reasons() {
let agent_mail = AgentMailCoordinationInput::Available {
reservations: vec![AgentMailReservation {
path_pattern: "src/core/lib.rs".to_owned(),
holder_agent: "OtherAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T09:00:00Z".to_owned()),
reservation_id: Some("reservation-1".to_owned()),
bead_id: None,
thread_id: None,
}],
active_agents: Vec::new(),
};
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
status_entry("src/core/lib.rs", ".", "M"),
status_entry("src/core/workspace.rs", ".", "M"),
status_entry("tests/workspace_hygiene.rs", ".", "M"),
status_entry("tests/fixtures/golden/workspace.json", ".", "M"),
status_entry("docs/agent-ux/workspace-hygiene.md", ".", "M"),
]),
&agent_mail,
BeadsMetadataSignal::Unknown,
&[],
);
let group_names = report
.staging_groups
.iter()
.map(|group| group.name.as_str())
.collect::<Vec<_>>();
assert_eq!(group_names, vec!["docs", "goldens", "source", "tests"]);
assert!(
report.staging_groups.iter().all(|group| group.read_only),
"recommendations must remain read-only"
);
assert!(
report
.staging_groups
.iter()
.all(|group| !group.paths.iter().any(|path| path == "src/core/lib.rs")),
"coordination-blocked paths must not be stage recommendations"
);
let source = report
.staging_groups
.iter()
.find(|group| group.name == "source")
.expect("source group");
assert_eq!(source.paths, vec!["src/core/workspace.rs"]);
assert_eq!(source.path_count, 1);
assert_eq!(source.kinds, vec!["source"]);
assert!(source.reasons.contains(&"src_rust_source".to_owned()));
assert_eq!(
source.recommendation,
"review_and_stage_as_one_logical_commit"
);
}
#[test]
fn hygiene_recommendations_keep_beads_only_metadata_out_of_fast_stage_groups() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![status_entry(BEADS_JSONL_RELATIVE_PATH, ".", "M")]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert!(report.staging_groups.is_empty());
assert!(report.do_not_commit.is_empty());
assert!(report.needs_human_review.is_empty());
assert_eq!(report.classifications.len(), 1);
assert_eq!(report.classifications[0].bucket, Bucket::IgnoreForNow);
assert_eq!(report.classifications[0].kind, Kind::BeadsMetadata);
assert!(report.read_only);
}
#[test]
fn hygiene_recommendations_keep_scratch_only_workspaces_out_of_staging() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
untracked_status_entry("drift-report.txt"),
untracked_status_entry("ubs.json"),
]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert!(report.staging_groups.is_empty());
assert_eq!(report.do_not_commit, vec!["drift-report.txt", "ubs.json"]);
assert!(report.needs_human_review.is_empty());
assert!(
report
.classifications
.iter()
.all(|row| row.bucket == Bucket::DoNotCommit && row.kind == Kind::Scratch)
);
assert!(
report
.next_actions
.iter()
.any(|action| action.contains("Leave doNotCommit paths unstaged")),
"scratch-only report should tell agents not to stage scratch paths"
);
assert!(report.read_only);
}
#[test]
fn hygiene_recommendations_keep_secret_risk_paths_out_of_staging() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
untracked_status_entry(".env.local"),
status_entry("configs/secrets.toml", ".", "M"),
status_entry("src/core/workspace.rs", ".", "M"),
]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let recommended_paths = report
.staging_groups
.iter()
.flat_map(|group| group.paths.iter())
.map(String::as_str)
.collect::<Vec<_>>();
assert_eq!(recommended_paths, vec!["src/core/workspace.rs"]);
assert_eq!(report.do_not_commit, vec![".env.local"]);
assert_eq!(report.needs_human_review, vec!["configs/secrets.toml"]);
let env_row = report
.classifications
.iter()
.find(|row| row.path == ".env.local")
.expect("env file classification");
assert_eq!(env_row.kind, Kind::SecretRisk);
assert_eq!(env_row.bucket, Bucket::DoNotCommit);
let tracked_secret_row = report
.classifications
.iter()
.find(|row| row.path == "configs/secrets.toml")
.expect("tracked secret classification");
assert_eq!(tracked_secret_row.kind, Kind::SecretRisk);
assert_eq!(tracked_secret_row.bucket, Bucket::NeedsHumanReview);
assert!(report.read_only);
}
#[test]
fn hygiene_recommendations_single_logical_commit_groups_only_source() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
status_entry("src/core/workspace.rs", ".", "M"),
status_entry("src/core/hygiene_classifier.rs", ".", "M"),
status_entry("src/cli/mod.rs", ".", "M"),
]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let group_names: Vec<&str> = report
.staging_groups
.iter()
.map(|group| group.name.as_str())
.collect();
assert_eq!(
group_names,
vec!["source"],
"single-logical-commit scenario must produce one staging group"
);
let source = &report.staging_groups[0];
assert_eq!(source.path_count, 3);
assert_eq!(
source.paths,
vec![
"src/cli/mod.rs",
"src/core/hygiene_classifier.rs",
"src/core/workspace.rs",
],
"paths must be sorted deterministically inside the single source group"
);
assert_eq!(source.kinds, vec!["source"]);
assert_eq!(
source.recommendation,
"review_and_stage_as_one_logical_commit"
);
assert!(source.read_only);
assert!(report.do_not_commit.is_empty());
assert!(report.needs_human_review.is_empty());
assert!(report.coordination.blocked_by_coordination.is_empty());
assert!(report.read_only);
}
#[test]
fn hygiene_recommendations_split_mixed_source_and_scratch_into_disjoint_lanes() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
status_entry("src/core/workspace.rs", ".", "M"),
status_entry("src/cli/mod.rs", ".", "M"),
untracked_status_entry("drift-report.txt"),
untracked_status_entry("ubs.json"),
untracked_status_entry(".plan-drift-report.json"),
]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let staged_paths: Vec<&str> = report
.staging_groups
.iter()
.flat_map(|group| group.paths.iter().map(String::as_str))
.collect();
assert_eq!(
staged_paths,
vec!["src/cli/mod.rs", "src/core/workspace.rs"],
"mixed source+scratch must keep source in staging only"
);
assert_eq!(
report.do_not_commit,
vec![".plan-drift-report.json", "drift-report.txt", "ubs.json"],
"mixed source+scratch must route scratch paths to doNotCommit"
);
for staged in &staged_paths {
assert!(
!report.do_not_commit.contains(&(*staged).to_owned()),
"{staged} must not appear in both staging and doNotCommit"
);
}
for scratch in &report.do_not_commit {
assert!(
!staged_paths.contains(&scratch.as_str()),
"{scratch} must not be promoted into a staging group"
);
}
assert!(report.needs_human_review.is_empty());
assert!(report.read_only);
}
#[test]
fn hygiene_recommendations_realistic_multi_agent_workspace_holds_invariants() {
let agent_mail = AgentMailCoordinationInput::Available {
reservations: vec![AgentMailReservation {
path_pattern: "src/core/curate.rs".to_owned(),
holder_agent: "OtherAgent".to_owned(),
exclusive: true,
expires_at: Some("2099-01-01T00:00:00Z".to_owned()),
reservation_id: Some("res-2".to_owned()),
bead_id: Some("bd-other".to_owned()),
thread_id: None,
}],
active_agents: vec![ActiveAgent {
name: "OtherAgent".to_owned(),
last_active_at: Some("2026-05-19T22:00:00Z".to_owned()),
}],
};
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
status_entry("src/core/workspace.rs", ".", "M"),
status_entry("src/core/curate.rs", ".", "M"),
status_entry("tests/workspace_hygiene_staging_e2e.rs", ".", "M"),
status_entry("docs/agent-ux/workspace-hygiene.md", ".", "M"),
status_entry("tests/fixtures/golden/workspace.json", ".", "M"),
status_entry(BEADS_JSONL_RELATIVE_PATH, ".", "M"),
untracked_status_entry("drift-report.txt"),
untracked_status_entry("ubs.json"),
status_entry("configs/secrets.toml", ".", "M"),
]),
&agent_mail,
BeadsMetadataSignal::Unknown,
&[],
);
let group_names: Vec<&str> = report
.staging_groups
.iter()
.map(|group| group.name.as_str())
.collect();
assert_eq!(
group_names,
vec!["docs", "goldens", "source", "tests"],
"realistic multi-agent workspace must split four logical commit slices"
);
let staged_paths: Vec<&str> = report
.staging_groups
.iter()
.flat_map(|group| group.paths.iter().map(String::as_str))
.collect();
assert!(
!staged_paths.contains(&"src/core/curate.rs"),
"coordination-blocked source path must not appear in staging"
);
assert!(
staged_paths.contains(&"src/core/workspace.rs"),
"unblocked source path must appear in staging"
);
assert!(
!staged_paths.contains(&BEADS_JSONL_RELATIVE_PATH),
"Beads JSONL metadata must never be a staging recommendation"
);
assert!(
!staged_paths.iter().any(|path| *path == "drift-report.txt"
|| *path == "ubs.json"
|| *path == "configs/secrets.toml"),
"scratch + secret-risk paths must never appear in staging"
);
assert!(
report
.do_not_commit
.contains(&"drift-report.txt".to_owned())
&& report.do_not_commit.contains(&"ubs.json".to_owned()),
"untracked scratch reports must be in doNotCommit"
);
assert!(
report
.needs_human_review
.contains(&"configs/secrets.toml".to_owned()),
"tracked secret-looking path must surface in needsHumanReview"
);
assert_eq!(
report.coordination.blocked_by_coordination.len(),
1,
"exactly one coordination block for the reserved source path"
);
assert_eq!(
report.coordination.blocked_by_coordination[0].path,
"src/core/curate.rs"
);
assert_eq!(
report.coordination.blocked_by_coordination[0].holder_agent,
"OtherAgent"
);
assert!(
report.coordination.agent_mail_available,
"agent mail availability must reflect the precollected snapshot"
);
assert!(report.read_only);
for group in &report.staging_groups {
assert!(group.read_only, "group {} must be read-only", group.name);
}
let serialized = serde_json::to_string(&report).expect("serialize hygiene report");
for forbidden in [
"git add",
"git stash",
"git reset",
"git checkout",
"rm -rf",
] {
assert!(
!serialized.contains(forbidden),
"hygiene report must never emit mutation command {forbidden}"
);
}
}
#[test]
fn hygiene_secret_scan_collects_redacted_content_evidence() -> TestResult {
let workspace = unique_dir("ee-workspace-hygiene-secret-scan")?;
let raw_value = concat!(
"sk",
"-",
"proj-",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
);
write_file(
&workspace.join("notes.txt"),
&format!("ordinary notes\nOPENAI_API_KEY={raw_value}\n"),
)?;
let report = hygiene_report_from_workspace_parts(
&workspace,
hygiene_snapshot(vec![file_untracked_status_entry(
"notes.txt",
u64::try_from(format!("ordinary notes\nOPENAI_API_KEY={raw_value}\n").len())
.unwrap_or(u64::MAX),
)]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let row = report
.classifications
.iter()
.find(|row| row.path == "notes.txt")
.expect("notes classification");
assert_eq!(row.kind, Kind::SecretRisk);
assert_eq!(row.bucket, Bucket::DoNotCommit);
assert!(row.reasons.contains(&"secret_content_evidence"));
assert!(!row.redacted_evidence.is_empty());
assert_eq!(report.secret_scan.scanned_file_count, 1);
assert_eq!(
report.secret_scan.max_file_bytes,
WORKSPACE_SECRET_RISK_DEFAULT_MAX_SCAN_BYTES
);
assert_eq!(
report.secret_scan.max_total_bytes,
WORKSPACE_HYGIENE_SECRET_SCAN_MAX_TOTAL_BYTES
);
let rendered = serde_json::to_string(&report).expect("workspace hygiene JSON");
assert!(
!rendered.contains(raw_value),
"workspace hygiene report must not leak raw content secret"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_SECRET_SCAN_SKIPPED_CODE)
);
Ok(())
}
#[test]
fn hygiene_secret_scan_enforces_file_count_and_total_byte_budgets() -> TestResult {
let workspace = unique_dir("ee-workspace-hygiene-secret-budget")?;
write_file(&workspace.join("a.txt"), "alpha")?;
write_file(&workspace.join("b.txt"), "bravo")?;
write_file(&workspace.join("large.txt"), "0123456789")?;
let snapshot = hygiene_snapshot(vec![
file_status_entry("a.txt", ".", "M", 5),
file_status_entry("b.txt", ".", "M", 5),
file_status_entry("large.txt", ".", "M", 10),
]);
let (lookup, summary) = workspace_hygiene_secret_evidence_with_budget(
&workspace,
&snapshot,
WorkspaceHygieneSecretScanBudget {
max_files: 10,
max_file_bytes: 8,
max_total_bytes: 5,
},
);
assert!(lookup.is_empty());
assert_eq!(summary.scanned_file_count, 1);
assert_eq!(summary.scanned_byte_count, 5);
assert_eq!(
summary.skipped_content_scan_count, 2,
"one path should exceed max_total_bytes and one should exceed max_file_bytes"
);
let (_, file_count_summary) = workspace_hygiene_secret_evidence_with_budget(
&workspace,
&snapshot,
WorkspaceHygieneSecretScanBudget {
max_files: 1,
max_file_bytes: 8,
max_total_bytes: 20,
},
);
assert_eq!(file_count_summary.scanned_file_count, 1);
assert_eq!(file_count_summary.scanned_byte_count, 5);
assert_eq!(
file_count_summary.skipped_content_scan_count, 2,
"one path should hit max_files and one should exceed max_file_bytes"
);
let report = workspace_hygiene_secret_scan_report(
WorkspaceHygieneSecretScanBudget {
max_files: 1,
max_file_bytes: 8,
max_total_bytes: 20,
},
file_count_summary,
);
assert!(report.read_only);
assert_eq!(report.max_files, 1);
assert_eq!(report.max_file_bytes, 8);
assert_eq!(report.max_total_bytes, 20);
assert_eq!(report.skipped_content_scan_count, 2);
Ok(())
}
#[test]
fn hygiene_report_records_10k_path_perf_contract_size_proxy() {
let entries = (0..10_000)
.map(|index| status_entry(&format!("src/perf/file_{index:05}.rs"), ".", "M"))
.collect::<Vec<_>>();
let started = std::time::Instant::now();
let report = hygiene_report_from_parts(
hygiene_snapshot(entries),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
let serialized = serde_json::to_string(&report).expect("workspace hygiene JSON");
eprintln!(
"{}",
serde_json::json!({
"schema": "ee.test_event.v1",
"beadId": "bd-1eq3l.13",
"surface": "workspace_hygiene",
"phase": "perf_contract",
"pathCount": 10_000,
"elapsedMs": elapsed_ms,
"serializedBytes": serialized.len(),
"pathClassificationCount": report.classifications.len(),
"stagingGroupCount": report.staging_groups.len(),
"truncated": report.output_truncation.truncated,
})
);
assert_eq!(report.dirty_path_count, 10_000);
assert_eq!(report.git_summary.dirty_path_count, 10_000);
assert_eq!(report.classifications.len(), 10_000);
assert!(
!report.output_truncation.truncated,
"10k fixture sits exactly at the default visible cap and must not truncate"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE)
);
assert_eq!(report.staging_groups.len(), 1);
let source_group = &report.staging_groups[0];
assert_eq!(source_group.name, "source");
assert_eq!(source_group.path_count, 10_000);
assert_eq!(source_group.paths.len(), 10_000);
assert!(!source_group.paths_truncated);
assert_eq!(source_group.omitted_path_count, 0);
assert_eq!(source_group.paths[0], "src/perf/file_00000.rs");
assert_eq!(
source_group.paths.last().map(String::as_str),
Some("src/perf/file_09999.rs")
);
assert!(
serialized.len() < 5_000_000,
"10k workspace hygiene report should stay within the perf-contract size proxy, got {} bytes",
serialized.len()
);
}
#[test]
fn hygiene_report_truncates_large_path_arrays_deterministically() {
let entries = (0..100_050)
.map(|index| status_entry(&format!("src/generated/file_{index:06}.rs"), ".", "M"))
.collect::<Vec<_>>();
let report = hygiene_report_from_parts(
hygiene_snapshot(entries),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert_eq!(report.dirty_path_count, 100_050);
assert_eq!(report.git_summary.dirty_path_count, 100_050);
assert_eq!(
report.classifications.len(),
WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS
);
assert!(report.output_truncation.truncated);
assert_eq!(
report.output_truncation.omitted_path_classifications,
100_050 - WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS
);
assert_eq!(report.output_truncation.omitted_do_not_commit, 0);
assert_eq!(report.output_truncation.omitted_needs_human_review, 0);
assert_eq!(
report.output_truncation.omitted_by_bucket,
vec![WorkspaceHygieneCount {
name: "stage_candidate".to_owned(),
count: 100_050 - WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS,
}]
);
assert_eq!(
report.output_truncation.omitted_by_kind,
vec![WorkspaceHygieneCount {
name: "source".to_owned(),
count: 100_050 - WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS,
}]
);
assert!(
report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE)
);
assert_eq!(report.staging_groups.len(), 1);
let source_group = &report.staging_groups[0];
assert_eq!(source_group.name, "source");
assert_eq!(source_group.path_count, 100_050);
assert!(source_group.paths_truncated);
assert_eq!(
source_group.paths.len(),
WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP
);
assert_eq!(
source_group.omitted_path_count,
100_050 - WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP
);
assert_eq!(source_group.paths[0], "src/generated/file_000000.rs");
assert_eq!(
source_group
.paths
.last()
.map(String::as_str)
.expect("last truncated path"),
"src/generated/file_009999.rs"
);
assert_eq!(
report.output_truncation.staging_groups[0].omitted_path_count,
100_050 - WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP
);
assert!(
report
.next_actions
.iter()
.any(|action| action.contains("outputTruncation")),
"truncated reports should point agents at outputTruncation details"
);
let serialized = serde_json::to_string(&report).expect("workspace hygiene JSON");
assert!(
serialized.len() < 8_000_000,
"serialized report should stay within the large-report output budget, got {} bytes",
serialized.len()
);
assert!(serialized.contains("\"outputTruncation\""));
assert!(serialized.contains("\"pathsTruncated\":true"));
assert!(
!serialized.contains("src/generated/file_010000.rs"),
"paths beyond the deterministic visible prefix must be omitted"
);
}
#[test]
fn hygiene_report_truncates_commit_warning_lists_deterministically() {
let mut entries = (0..10_050)
.map(|index| untracked_status_entry(&format!("drift-report-{index:05}.txt")))
.collect::<Vec<_>>();
entries.extend(
(0..10_025)
.map(|index| status_entry(&format!("configs/{index:05}/secrets.toml"), ".", "M")),
);
let report = hygiene_report_from_parts(
hygiene_snapshot(entries),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert_eq!(report.dirty_path_count, 20_075);
assert_eq!(
report.do_not_commit.len(),
WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST
);
assert_eq!(
report.needs_human_review.len(),
WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST
);
assert_eq!(report.do_not_commit[0], "drift-report-00000.txt");
assert_eq!(
report.do_not_commit.last().map(String::as_str),
Some("drift-report-09999.txt")
);
assert_eq!(report.needs_human_review[0], "configs/00000/secrets.toml");
assert_eq!(
report.needs_human_review.last().map(String::as_str),
Some("configs/09999/secrets.toml")
);
assert!(report.output_truncation.truncated);
assert_eq!(report.output_truncation.omitted_do_not_commit, 50);
assert_eq!(report.output_truncation.omitted_needs_human_review, 25);
let swarm_summary = WorkspaceHygieneSwarmBriefSummary::from_report(&report);
assert_eq!(swarm_summary.needs_human_review_top.len(), 10);
assert_eq!(swarm_summary.needs_human_review_total, 10_025);
assert!(swarm_summary.needs_human_review_truncated);
assert_eq!(
swarm_summary
.needs_human_review_top
.last()
.map(String::as_str),
Some("configs/00009/secrets.toml")
);
assert!(
report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE)
);
assert!(
report
.next_actions
.iter()
.any(|action| action.contains("outputTruncation")),
"truncated warning lists should point agents at outputTruncation details"
);
let serialized = serde_json::to_string(&report).expect("workspace hygiene JSON");
assert!(serialized.contains("\"omittedDoNotCommit\":50"));
assert!(serialized.contains("\"omittedNeedsHumanReview\":25"));
assert!(
!serialized.contains("drift-report-10000.txt"),
"doNotCommit paths beyond the visible prefix must be omitted"
);
assert!(
!serialized.contains("configs/10000/secrets.toml"),
"needsHumanReview paths beyond the visible prefix must be omitted"
);
}
#[test]
fn symbol_risk_summary_is_redaction_safe_and_embeds_in_swarm_brief() -> TestResult {
let mut report = hygiene_report_from_parts(
hygiene_snapshot(vec![status_entry("src/core/search.rs", ".", "M")]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let snapshot = crate::core::symbol_graph::extract_rust_symbol_snapshot_from_sources(&[
crate::core::symbol_graph::RustSourceInput::new(
"src/core/search.rs",
"pub fn run_search_command() {}\nfn private_helper() {}\n",
),
]);
let links = crate::core::symbol_graph::link_symbol_evidence(
&snapshot,
&[crate::core::symbol_graph::SymbolEvidenceInput::new(
SymbolEvidenceSourceKind::Failure,
"failure-memory-1",
"memory://failure-1",
"src/core/search.rs",
1,
1,
0.92,
)],
);
attach_workspace_hygiene_symbol_risk_summary(
&mut report,
Some(&snapshot),
Some(&links),
&[WorkspaceHygieneSymbolAgentActivity {
path: "src/core/search.rs",
agent_name: "SapphireHill",
}],
);
let summary = report
.symbol_risk_summary
.as_ref()
.ok_or_else(|| "symbol risk summary missing".to_string())?;
assert_eq!(summary.schema, WORKSPACE_HYGIENE_SYMBOL_RISK_SCHEMA_V1);
assert_eq!(summary.status, "available");
assert_eq!(summary.dirty_path_count, 1);
assert_eq!(summary.summarized_path_count, 1);
assert_eq!(summary.linked_evidence_count, 1);
assert_eq!(summary.recent_agent_activity_count, 1);
assert!(
summary.high_risk_symbol_count >= 1,
"public function should be counted as a high-risk public surface"
);
let path = &summary.paths[0];
assert_eq!(path.path, "src/core/search.rs");
assert!(path.path_hash.starts_with("blake3:"));
assert_eq!(path.evidence_source_kinds, vec!["failure"]);
assert_eq!(path.agent_name_hashes.len(), 1);
assert!(path.agent_name_hashes[0].starts_with("blake3:"));
assert!(
path.symbols
.iter()
.any(|symbol| symbol.public_surface && symbol.kind == "cli_command_handler"),
"CLI handler symbol should be surfaced as a high-risk public surface"
);
assert!(
path.symbols
.iter()
.all(|symbol| symbol.symbol_id_hash.starts_with("blake3:")
&& symbol.canonical_name_hash.starts_with("blake3:")),
"symbol identifiers and names must be hash-only"
);
let json = serde_json::to_string(summary).map_err(|error| error.to_string())?;
assert!(
!json.contains("run_search_command"),
"summary must not expose raw symbol names"
);
assert!(
!json.contains("SapphireHill"),
"summary must not expose raw agent names"
);
let swarm_summary = WorkspaceHygieneSwarmBriefSummary::from_report(&report);
assert!(
swarm_summary.symbol_risk_summary.is_some(),
"swarm brief projection should carry attached symbol risk summary"
);
Ok(())
}
#[test]
fn clean_workspace_hygiene_report_serializes_to_pinned_response_envelope_shape() {
let agent_mail_input = AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
};
let report = build_workspace_hygiene_report_from_inputs(WorkspaceHygieneReportInputs {
workspace_path: Path::new("/repo"),
snapshot: hygiene_snapshot(Vec::new()),
classifier_config: &HygieneClassifierConfig::default(),
jsonl_content: None,
self_agent_name: None,
beads_metadata_signal: BeadsMetadataSignal::Unknown,
beads_reservations: &[],
agent_mail_input: &agent_mail_input,
now: DateTime::parse_from_rfc3339("2026-05-18T08:00:00Z")
.expect("valid test timestamp")
.with_timezone(&Utc),
});
assert_eq!(report.schema, WORKSPACE_HYGIENE_SCHEMA_V1);
assert_eq!(report.command, "workspace hygiene");
assert!(
report.read_only,
"clean envelope must self-declare read-only"
);
assert_eq!(report.workspace_path, "/repo");
assert_eq!(report.repository_root, "/repo");
assert_eq!(report.dirty_path_count, 0);
assert!(report.bucket_counts.is_empty());
assert!(report.kind_counts.is_empty());
assert!(report.staging_groups.is_empty());
assert!(report.classifications.is_empty());
assert!(report.do_not_commit.is_empty());
assert!(report.needs_human_review.is_empty());
assert!(
report.next_actions.is_empty(),
"clean fixture must produce no nextActions noise: {:?}",
report.next_actions
);
assert!(
!report.output_truncation.truncated,
"clean fixture must not report truncation"
);
assert_eq!(
report.output_truncation.max_path_classifications,
WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS
);
assert_eq!(
report.output_truncation.max_paths_per_list,
WORKSPACE_HYGIENE_MAX_PATHS_PER_LIST
);
assert_eq!(
report.output_truncation.max_paths_per_staging_group,
WORKSPACE_HYGIENE_MAX_PATHS_PER_STAGING_GROUP
);
assert_eq!(report.output_truncation.omitted_path_classifications, 0);
assert_eq!(report.output_truncation.omitted_do_not_commit, 0);
assert_eq!(report.output_truncation.omitted_needs_human_review, 0);
assert!(report.output_truncation.omitted_by_bucket.is_empty());
assert!(report.output_truncation.omitted_by_kind.is_empty());
assert!(report.output_truncation.staging_groups.is_empty());
assert!(
report.secret_scan.read_only,
"secret scan reports must self-declare read-only"
);
assert_eq!(report.secret_scan.scanned_file_count, 0);
assert_eq!(report.secret_scan.scanned_byte_count, 0);
assert_eq!(report.secret_scan.skipped_content_scan_count, 0);
assert_eq!(
report.secret_scan.max_file_bytes,
WORKSPACE_SECRET_RISK_DEFAULT_MAX_SCAN_BYTES
);
assert_eq!(
report.degraded_codes.as_slice(),
&[WORKSPACE_HYGIENE_PARTIAL_METADATA_CODE],
"clean fixture must only carry the always-on partial-metadata caveat"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_AGENT_MAIL_UNAVAILABLE_CODE),
"Available agent_mail input must not raise agent_mail_unavailable"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_OUTPUT_TRUNCATED_CODE),
"clean fixture must not raise output_truncated"
);
assert!(
!report
.degraded_codes
.contains(&WORKSPACE_HYGIENE_SECRET_SCAN_SKIPPED_CODE),
"clean fixture must not raise secret_scan_skipped"
);
let envelope = serde_json::json!({
"schema": crate::models::RESPONSE_SCHEMA_V2,
"success": true,
"data": report,
});
assert_eq!(
envelope.pointer("/schema").and_then(Value::as_str),
Some(crate::models::RESPONSE_SCHEMA_V2),
"top-level envelope schema must match the current response contract"
);
assert_eq!(
envelope.pointer("/success").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
envelope.pointer("/data/schema").and_then(Value::as_str),
Some(WORKSPACE_HYGIENE_SCHEMA_V1)
);
assert_eq!(
envelope.pointer("/data/command").and_then(Value::as_str),
Some("workspace hygiene")
);
assert_eq!(
envelope.pointer("/data/readOnly").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
envelope.pointer("/data/workspace").and_then(Value::as_str),
Some("/repo")
);
assert_eq!(
envelope
.pointer("/data/repositoryRoot")
.and_then(Value::as_str),
Some("/repo")
);
assert_eq!(
envelope
.pointer("/data/dirtyPathCount")
.and_then(Value::as_u64),
Some(0)
);
for key in [
"/data/bucketCounts",
"/data/kindCounts",
"/data/stagingRecommendations",
"/data/pathClassifications",
"/data/doNotCommit",
"/data/needsHumanReview",
"/data/nextActions",
] {
let array = envelope.pointer(key).and_then(Value::as_array);
let array = array.unwrap_or_else(|| {
panic!("{key} must serialize as a JSON array in the clean envelope")
});
assert!(
array.is_empty(),
"{key} must be empty for a clean workspace, got {array:?}"
);
}
let degraded_codes = envelope
.pointer("/data/degraded")
.and_then(Value::as_array)
.unwrap_or_else(|| panic!("/data/degraded must serialize as a JSON array"));
let codes: Vec<&str> = degraded_codes.iter().filter_map(Value::as_str).collect();
assert_eq!(
codes.as_slice(),
&[WORKSPACE_HYGIENE_PARTIAL_METADATA_CODE],
"clean envelope must carry exactly the partial-metadata caveat"
);
assert_eq!(
envelope
.pointer("/data/gitSummary/repositoryRoot")
.and_then(Value::as_str),
Some("/repo")
);
assert_eq!(
envelope
.pointer("/data/gitSummary/dirtyPathCount")
.and_then(Value::as_u64),
Some(0)
);
assert!(
envelope
.pointer("/data/gitSummary/bucketCounts")
.and_then(Value::as_array)
.map(Vec::is_empty)
.unwrap_or(false),
"gitSummary.bucketCounts must be an empty array for a clean workspace"
);
assert_eq!(
envelope
.pointer("/data/outputTruncation/truncated")
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
envelope
.pointer("/data/outputTruncation/maxPathClassifications")
.and_then(Value::as_u64),
Some(WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS as u64)
);
assert_eq!(
envelope
.pointer("/data/secretScan/readOnly")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
envelope
.pointer("/data/secretScan/scannedFileCount")
.and_then(Value::as_u64),
Some(0)
);
assert_eq!(
envelope
.pointer("/data/coordinationState/agentMailAvailable")
.and_then(Value::as_bool),
Some(true),
"coordinationState must reflect the Available agent-mail input"
);
assert!(
envelope.pointer("/data/beadsState").is_some(),
"beadsState must serialize even when jsonl_content is absent"
);
let pretty = serde_json::to_string_pretty(&envelope)
.expect("clean envelope must serialize to pretty JSON without panic");
assert!(
pretty.contains("\"command\": \"workspace hygiene\""),
"pretty envelope must include the canonical command identifier"
);
assert!(
!pretty.contains("\"truncated\": true"),
"clean fixture must not flip any truncated=true flag: {pretty}"
);
}
#[test]
fn hygiene_agent_harness_advisory_is_success_by_default() {
let mut large_binary = status_entry("artifacts/result.bin", ".", "M");
large_binary.metadata = Some(WorkspaceGitPathMetadata {
exists: true,
file_type: "file".to_owned(),
size_bytes: Some(2_000_000),
large_file: true,
skip_reason: Some("binary".to_owned()),
});
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![untracked_status_entry(".env"), large_binary]),
&AgentMailCoordinationInput::Unavailable,
BeadsMetadataSignal::Unknown,
&[],
);
let advisory = workspace_hygiene_agent_harness_advisory(&report, false);
assert_eq!(advisory.schema, WORKSPACE_HYGIENE_SCHEMA_V1);
assert_eq!(advisory.payload_schema, WORKSPACE_HYGIENE_SCHEMA_V1);
assert_eq!(
advisory.target,
WORKSPACE_HYGIENE_AGENT_ADVISORY_TARGET_PRECOMMIT
);
assert!(advisory.read_only);
assert!(!advisory.strict);
assert_eq!(advisory.status, "would_fail_strict");
assert_eq!(advisory.recommended_exit_code, 0);
let codes = advisory
.reasons
.iter()
.map(|reason| reason.code)
.collect::<Vec<_>>();
assert_eq!(codes, vec!["secret_risk", "unknown_high_risk_binary"]);
}
#[test]
fn hygiene_agent_harness_detects_high_risk_rows_omitted_from_visible_classifications() {
let mut entries = (0..WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS)
.map(|index| status_entry(&format!("src/visible/file_{index:05}.rs"), ".", "M"))
.collect::<Vec<_>>();
entries.push(status_entry("zzzz/secrets.toml", ".", "M"));
let mut large_binary = status_entry("zzzz/result.bin", ".", "M");
large_binary.metadata = Some(WorkspaceGitPathMetadata {
exists: true,
file_type: "file".to_owned(),
size_bytes: Some(2_000_000),
large_file: true,
skip_reason: Some("binary".to_owned()),
});
entries.push(large_binary);
let report = hygiene_report_from_parts(
hygiene_snapshot(entries),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert_eq!(
report.classifications.len(),
WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS
);
assert!(
report
.classifications
.iter()
.all(|row| row.kind == Kind::Source),
"visible classifications should contain only the sorted source prefix"
);
assert!(report.output_truncation.truncated);
let advisory = workspace_hygiene_agent_harness_advisory(&report, true);
assert_eq!(advisory.recommended_exit_code, 6);
let secret = advisory
.reasons
.iter()
.find(|reason| reason.code == "secret_risk")
.expect("secret risk reason");
assert_eq!(secret.paths, vec!["zzzz/secrets.toml".to_owned()]);
let binary = advisory
.reasons
.iter()
.find(|reason| reason.code == "unknown_high_risk_binary")
.expect("binary reason");
assert_eq!(binary.paths, vec!["zzzz/result.bin".to_owned()]);
}
#[test]
fn hygiene_agent_harness_strict_mode_reports_failure_reasons() {
let agent_mail = AgentMailCoordinationInput::Available {
reservations: vec![AgentMailReservation {
path_pattern: "src/core/workspace.rs".to_owned(),
holder_agent: "OtherAgent".to_owned(),
exclusive: true,
expires_at: Some("2026-05-18T09:00:00Z".to_owned()),
reservation_id: Some("reservation-1".to_owned()),
bead_id: Some("bd-1eq3l.12".to_owned()),
thread_id: Some("bd-1eq3l.12".to_owned()),
}],
active_agents: Vec::new(),
};
let report = hygiene_report_from_parts_with_jsonl(
hygiene_snapshot(vec![
status_entry("src/core/workspace.rs", ".", "M"),
status_entry(BEADS_JSONL_RELATIVE_PATH, ".", "M"),
]),
&agent_mail,
BeadsMetadataSignal::DbDirtyPendingFlush,
&[],
Some(b"{\"id\":\"bd-test\"}\nnot-json\n"),
);
let advisory = workspace_hygiene_agent_harness_advisory(&report, true);
assert!(advisory.strict);
assert_eq!(advisory.status, "strict_failed");
assert_eq!(advisory.recommended_exit_code, 6);
let codes = advisory
.reasons
.iter()
.map(|reason| reason.code)
.collect::<Vec<_>>();
assert_eq!(
codes,
vec!["active_reservation", "beads_conflict", "parse_error"]
);
assert!(
advisory
.reasons
.iter()
.all(|reason| !reason.paths.is_empty()),
"strict failure reasons must be machine-actionable: {advisory:#?}"
);
}
#[test]
fn hygiene_agent_harness_detects_scratch_only_commit() {
let report = hygiene_report_from_parts(
hygiene_snapshot(vec![
status_entry("drift-report.txt", ".", "M"),
status_entry("line-length-probe-output.txt", ".", "M"),
]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
let advisory = workspace_hygiene_agent_harness_advisory(&report, true);
assert_eq!(advisory.recommended_exit_code, 6);
assert_eq!(advisory.reason_count, 1);
assert_eq!(advisory.reasons[0].code, "scratch_only_commit");
assert_eq!(
advisory.reasons[0].paths,
vec![
"drift-report.txt".to_owned(),
"line-length-probe-output.txt".to_owned()
]
);
}
#[test]
fn hygiene_agent_harness_detects_truncated_scratch_only_commit() {
let entries = (0..=WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS)
.map(|index| status_entry(&format!("drift-report-{index:05}.txt"), ".", "M"))
.collect::<Vec<_>>();
let report = hygiene_report_from_parts(
hygiene_snapshot(entries),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
assert_eq!(
report.classifications.len(),
WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS
);
assert_eq!(
report.dirty_path_count,
WORKSPACE_HYGIENE_MAX_PATH_CLASSIFICATIONS + 1
);
assert!(report.output_truncation.truncated);
let advisory = workspace_hygiene_agent_harness_advisory(&report, true);
assert_eq!(advisory.recommended_exit_code, 6);
assert!(
advisory
.reasons
.iter()
.any(|reason| reason.code == "scratch_only_commit"),
"truncated scratch-only dirty set must still fail strict mode: {advisory:#?}"
);
}
#[test]
fn hygiene_report_can_embed_agent_harness_advisory_without_mutation() {
let mut report = hygiene_report_from_parts(
hygiene_snapshot(vec![status_entry("src/core/workspace.rs", ".", "M")]),
&AgentMailCoordinationInput::Available {
reservations: Vec::new(),
active_agents: Vec::new(),
},
BeadsMetadataSignal::Unknown,
&[],
);
attach_workspace_hygiene_agent_harness_advisory(&mut report, true);
let envelope = serde_json::json!({
"schema": crate::models::RESPONSE_SCHEMA_V2,
"success": true,
"data": report,
});
assert_eq!(
envelope.pointer("/data/schema").and_then(Value::as_str),
Some(WORKSPACE_HYGIENE_SCHEMA_V1),
"adapter keeps the same core workspace hygiene schema"
);
assert_eq!(
envelope
.pointer("/data/agentHarnessAdvisory/schema")
.and_then(Value::as_str),
Some(WORKSPACE_HYGIENE_SCHEMA_V1)
);
assert_eq!(
envelope
.pointer("/data/agentHarnessAdvisory/recommendedExitCode")
.and_then(Value::as_u64),
Some(0)
);
assert_eq!(
envelope
.pointer("/data/agentHarnessAdvisory/reasonCount")
.and_then(Value::as_u64),
Some(0)
);
}
}