use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Command;
use netsky_core::agent::AgentId;
use netsky_prompts::layers::{
PromptAgentKind, PromptContext, PromptLayer, PromptLayerKind, PromptLayerOrigin, compose,
resolve_catalog_layers,
};
use serde::Serialize;
use serde_json::json;
use crate::cli::{PromptOutputFormat, PromptsCommand};
const BYTES_PER_TOKEN_X10: usize = 33;
const LAYER_WARN_TOKENS: usize = 2_000;
const COMPOSED_WARN_TOKENS: usize = 12_000;
const SKILL_WARN_TOKENS: usize = 1_500;
const CADENCE_DENYLIST: &[&str] = &[
"we made a big update",
"in the time it takes",
"this change improves",
];
const CONTRADICTION_RULES: &[(&str, &str, &str)] = &[
(
"always use clones",
"do it inline",
"clone dispatch guidance disagrees",
),
("do not browse", "must browse", "browse guidance disagrees"),
(
"banned at clone level",
"requires",
"tool policy says both banned and required",
),
];
pub fn run(sub: PromptsCommand) -> netsky_core::Result<()> {
match sub {
PromptsCommand::Ls { agent, cwd, format } => ls(agent.as_deref(), cwd.as_deref(), format),
PromptsCommand::Cat { layer, agent, cwd } => cat(&layer, agent.as_deref(), cwd.as_deref()),
PromptsCommand::Diff {
layer,
commit,
agent,
cwd,
} => diff(&layer, &commit, agent.as_deref(), cwd.as_deref()),
PromptsCommand::Compose {
agent,
cwd,
skills,
format,
} => compose_cmd(agent.as_deref(), cwd.as_deref(), &skills, format),
PromptsCommand::Audit {
agent,
cwd,
skills,
format,
} => audit(agent.as_deref(), cwd.as_deref(), &skills, format),
}
}
fn ls(
agent: Option<&str>,
cwd: Option<&Path>,
format: PromptOutputFormat,
) -> netsky_core::Result<()> {
let query = PromptQuery::new(agent, cwd)?;
let layers = resolve_catalog_layers(query.context(), &query.cwd, &[])?;
match format {
PromptOutputFormat::Text => print_layers_text(&layers),
PromptOutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&layers)?);
}
}
Ok(())
}
fn cat(layer_id: &str, agent: Option<&str>, cwd: Option<&Path>) -> netsky_core::Result<()> {
let query = PromptQuery::new(agent, cwd)?;
let layers = resolve_catalog_layers(query.context(), &query.cwd, &[])?;
let layer = find_layer(&layers, layer_id)?;
if layer.body.is_empty() {
netsky_core::bail!("layer `{layer_id}` is empty");
}
print!("{}", layer.body);
if !layer.body.ends_with('\n') {
println!();
}
Ok(())
}
fn diff(
layer_id: &str,
commit: &str,
agent: Option<&str>,
cwd: Option<&Path>,
) -> netsky_core::Result<()> {
let query = PromptQuery::new(agent, cwd)?;
let layers = resolve_catalog_layers(query.context(), &query.cwd, &[])?;
let layer = find_layer(&layers, layer_id)?;
let Some(git_path) = layer.git_path.as_ref() else {
netsky_core::bail!("layer {layer_id} is not git-backed");
};
let output = Command::new("git")
.arg("-C")
.arg(&query.cwd)
.arg("diff")
.arg(format!("{commit}..HEAD"))
.arg("--")
.arg(git_path)
.output()
.map_err(|err| {
std::io::Error::other(format!("run git diff for {}: {err}", git_path.display()))
})?;
if !output.status.success() {
netsky_core::bail!("{}", String::from_utf8_lossy(&output.stderr).trim());
}
print!("{}", String::from_utf8_lossy(&output.stdout));
Ok(())
}
fn compose_cmd(
agent: Option<&str>,
cwd: Option<&Path>,
skills: &[String],
format: PromptOutputFormat,
) -> netsky_core::Result<()> {
let query = PromptQuery::new(agent, cwd)?;
let layers = resolve_catalog_layers(query.context(), &query.cwd, skills)?;
let active = active_layers(&layers);
let body = compose(&active)?;
match format {
PromptOutputFormat::Text => print!("{body}"),
PromptOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&json!({
"layers": active,
"body": body,
}))?
);
}
}
Ok(())
}
fn audit(
agent: Option<&str>,
cwd: Option<&Path>,
skills: &[String],
format: PromptOutputFormat,
) -> netsky_core::Result<()> {
let query = PromptQuery::new(agent, cwd)?;
let layers = resolve_catalog_layers(query.context(), &query.cwd, skills)?;
let report = build_audit_report(&query.cwd, &layers, skills);
match format {
PromptOutputFormat::Text => print_audit_text(&report),
PromptOutputFormat::Json => println!("{}", serde_json::to_string_pretty(&report)?),
}
if report
.findings
.iter()
.any(|finding| finding.severity == "error")
{
std::process::exit(1);
}
Ok(())
}
fn active_layers(layers: &[PromptLayer]) -> Vec<PromptLayer> {
layers
.iter()
.filter(|layer| layer.active)
.cloned()
.collect()
}
fn print_layers_text(layers: &[PromptLayer]) {
println!(
"{:<24} {:<16} {:<10} {:>6} {:<8} note",
"id", "kind", "origin", "bytes", "status"
);
for layer in layers {
let status = if layer.active { "active" } else { "catalog" };
println!(
"{:<24} {:<16} {:<10} {:>6} {:<8} {}",
layer.id,
kind_name(layer.kind),
origin_name(layer.origin),
layer.bytes(),
status,
layer.note.as_deref().unwrap_or("")
);
}
}
fn print_audit_text(report: &AuditReport) {
if report.findings.is_empty() {
println!("ok: no prompt audit findings");
return;
}
for finding in &report.findings {
println!(
"{}: {} [{}]",
finding.severity, finding.message, finding.layer_id
);
}
}
fn kind_name(kind: PromptLayerKind) -> &'static str {
match kind {
PromptLayerKind::Base => "base",
PromptLayerKind::Identity => "identity",
PromptLayerKind::CwdAddendum => "cwd_addendum",
PromptLayerKind::RuntimeAddendum => "runtime_addendum",
PromptLayerKind::Skill => "skill",
PromptLayerKind::HarnessDoc => "harness_doc",
PromptLayerKind::InstallAsset => "install_asset",
}
}
fn origin_name(origin: PromptLayerOrigin) -> &'static str {
match origin {
PromptLayerOrigin::LiveFile => "live",
PromptLayerOrigin::BundledFile => "bundled",
PromptLayerOrigin::RuntimeConfig => "runtime",
PromptLayerOrigin::Virtual => "virtual",
}
}
fn find_layer<'a>(
layers: &'a [PromptLayer],
layer_id: &str,
) -> netsky_core::Result<&'a PromptLayer> {
layers
.iter()
.find(|layer| layer.id == layer_id)
.ok_or_else(|| std::io::Error::other(format!("unknown layer `{layer_id}`")).into())
}
#[derive(Debug, Clone, Serialize)]
struct AuditReport {
findings: Vec<AuditFinding>,
}
#[derive(Debug, Clone, Serialize)]
struct AuditFinding {
severity: &'static str,
code: &'static str,
layer_id: String,
message: String,
}
fn build_audit_report(
cwd: &Path,
layers: &[PromptLayer],
requested_skills: &[String],
) -> AuditReport {
let mut findings = Vec::new();
let active = active_layers(layers);
let composed = compose(&active).unwrap_or_default();
let active_ids: BTreeSet<_> = active.iter().map(|layer| layer.id.as_str()).collect();
let mut paragraph_map: BTreeMap<String, String> = BTreeMap::new();
for layer in &active {
for paragraph in paragraphs(&layer.body) {
let normalized = normalize(¶graph);
if normalized.len() < 24 {
continue;
}
if let Some(first) = paragraph_map.get(&normalized) {
findings.push(AuditFinding {
severity: "warn",
code: "duplication",
layer_id: layer.id.clone(),
message: format!("duplicate paragraph also appears in {first}"),
});
} else {
paragraph_map.insert(normalized, layer.id.clone());
}
}
}
for layer in &active {
let tokens = estimated_tokens(&layer.body);
let limit = if layer.kind == PromptLayerKind::Skill {
SKILL_WARN_TOKENS
} else {
LAYER_WARN_TOKENS
};
if tokens > limit {
findings.push(AuditFinding {
severity: "warn",
code: "token_bloat",
layer_id: layer.id.clone(),
message: format!("estimated {tokens} tokens exceeds {limit}"),
});
}
for phrase in CADENCE_DENYLIST {
if normalize(&layer.body).contains(&normalize(phrase)) {
findings.push(AuditFinding {
severity: "warn",
code: "cadence",
layer_id: layer.id.clone(),
message: format!("contains denylisted cadence phrase `{phrase}`"),
});
}
}
}
let composed_limit = if requested_skills.is_empty() {
COMPOSED_WARN_TOKENS
} else {
usize::MAX
};
let composed_tokens = estimated_tokens(&composed);
if composed_tokens > composed_limit {
findings.push(AuditFinding {
severity: "warn",
code: "token_bloat",
layer_id: "compose".to_string(),
message: format!(
"estimated composed prompt {composed_tokens} tokens exceeds {COMPOSED_WARN_TOKENS}"
),
});
}
let normalized_layers: Vec<_> = active
.iter()
.map(|layer| (layer.id.clone(), normalize(&layer.body)))
.collect();
for (left, right, message) in CONTRADICTION_RULES {
let left_hits: Vec<_> = normalized_layers
.iter()
.filter(|(_, body)| body.contains(&normalize(left)))
.map(|(id, _)| id.clone())
.collect();
let right_hits: Vec<_> = normalized_layers
.iter()
.filter(|(_, body)| body.contains(&normalize(right)))
.map(|(id, _)| id.clone())
.collect();
if !left_hits.is_empty() && !right_hits.is_empty() {
findings.push(AuditFinding {
severity: "error",
code: "contradiction",
layer_id: format!("{},{}", left_hits.join(","), right_hits.join(",")),
message: message.to_string(),
});
}
}
for layer in layers {
if layer.kind == PromptLayerKind::InstallAsset && layer.id.starts_with("bundled:prompts:") {
let live_id = layer.id.replacen("bundled:prompts:", "", 1);
let live_id = if live_id == "base" {
"base".to_string()
} else if live_id == "agent0" {
"agent:root".to_string()
} else if live_id == "clone" {
"agent:clone".to_string()
} else if live_id == "agentinfinity" {
"agent:watchdog".to_string()
} else {
continue;
};
if let Some(live) = layers.iter().find(|candidate| candidate.id == live_id)
&& live.body != layer.body
{
findings.push(AuditFinding {
severity: "error",
code: "bundled_drift",
layer_id: layer.id.clone(),
message: format!("bundled asset drift from live layer {}", live.id),
});
}
}
}
let harness_live = [
("harness:agents", "AGENTS.md"),
("harness:claude", "CLAUDE.md"),
];
for (id, name) in harness_live {
let bundled_id = format!("bundled:{}", name.trim_end_matches(".md").to_lowercase());
let live = layers.iter().find(|layer| layer.id == id);
let bundled = layers.iter().find(|layer| layer.id == bundled_id);
if let (Some(live), Some(bundled)) = (live, bundled)
&& normalize(&live.body) != normalize(&bundled.body)
{
findings.push(AuditFinding {
severity: "warn",
code: "harness_mismatch",
layer_id: id.to_string(),
message: format!("{name} differs from bundled install asset"),
});
}
}
let expected = match inferred_addendum_file(agent_kind_from_layers(&active_ids), cwd) {
Some(path) => path,
None => cwd.join("0.md"),
};
if !expected.exists() && !active_ids.contains("cwd:addendum") {
findings.push(AuditFinding {
severity: "warn",
code: "missing_addendum",
layer_id: "cwd:addendum".to_string(),
message: format!("configured addendum missing at {}", expected.display()),
});
}
AuditReport { findings }
}
fn agent_kind_from_layers(active_ids: &BTreeSet<&str>) -> PromptAgentKind {
if active_ids.contains("agent:watchdog") {
PromptAgentKind::Agentinfinity
} else if active_ids.contains("agent:clone") {
PromptAgentKind::Clone
} else {
PromptAgentKind::Agent0
}
}
fn inferred_addendum_file(kind: PromptAgentKind, cwd: &Path) -> Option<PathBuf> {
let name = match kind {
PromptAgentKind::Agent0 => "0.md",
PromptAgentKind::Clone => return Some(cwd.join("1.md")),
PromptAgentKind::Agentinfinity => "agentinfinity.md",
};
Some(cwd.join(name))
}
fn paragraphs(body: &str) -> Vec<String> {
body.split("\n\n")
.map(str::trim)
.filter(|part| !part.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn normalize(body: &str) -> String {
body.to_ascii_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn estimated_tokens(body: &str) -> usize {
(body.len() * 10).div_ceil(BYTES_PER_TOKEN_X10)
}
#[derive(Clone)]
struct PromptQuery {
agent: AgentId,
cwd: PathBuf,
}
impl PromptQuery {
fn new(agent: Option<&str>, cwd: Option<&Path>) -> netsky_core::Result<Self> {
let cwd = match cwd {
Some(path) => path.to_path_buf(),
None => std::env::current_dir()?,
};
let agent = parse_agent(agent)?;
Ok(Self { agent, cwd })
}
fn context(&self) -> PromptContext<AgentId> {
PromptContext::new(self.agent, self.cwd.display().to_string())
}
}
fn parse_agent(agent: Option<&str>) -> netsky_core::Result<AgentId> {
match agent {
Some("0") | Some("agent0") => Ok(AgentId::Agent0),
Some("infinity") | Some("agentinfinity") => Ok(AgentId::Agentinfinity),
Some(raw) => {
let raw = raw.trim_start_matches("agent");
let n = raw.parse::<u32>()?;
Ok(AgentId::from_number(n))
}
None => match std::env::var("AGENT_N").ok().as_deref() {
Some("0") => Ok(AgentId::Agent0),
Some("infinity") => Ok(AgentId::Agentinfinity),
Some(raw) => Ok(AgentId::from_number(raw.parse::<u32>()?)),
None => Ok(AgentId::Agent0),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_exact_duplicate_paragraph() {
let report = build_audit_report(
Path::new("."),
&[
PromptLayer {
id: "base".to_string(),
kind: PromptLayerKind::Base,
origin: PromptLayerOrigin::LiveFile,
path: None,
git_path: None,
body: "this is a duplicated paragraph with enough words\n\nsecond".to_string(),
active: true,
note: None,
},
PromptLayer {
id: "skill:x".to_string(),
kind: PromptLayerKind::Skill,
origin: PromptLayerOrigin::LiveFile,
path: None,
git_path: None,
body: "this is a duplicated paragraph with enough words".to_string(),
active: true,
note: None,
},
],
&[],
);
assert!(report.findings.iter().any(|f| f.code == "duplication"));
}
#[test]
fn detects_contradiction_pair() {
let report = build_audit_report(
Path::new("."),
&[
PromptLayer {
id: "base".to_string(),
kind: PromptLayerKind::Base,
origin: PromptLayerOrigin::LiveFile,
path: None,
git_path: None,
body: "always use clones".to_string(),
active: true,
note: None,
},
PromptLayer {
id: "cwd:addendum".to_string(),
kind: PromptLayerKind::CwdAddendum,
origin: PromptLayerOrigin::LiveFile,
path: None,
git_path: None,
body: "do it inline".to_string(),
active: true,
note: None,
},
],
&[],
);
assert!(
report
.findings
.iter()
.any(|f| f.code == "contradiction" && f.severity == "error")
);
}
}