use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json;
use std::path::{Path, PathBuf};
const COORDINATION_DEFAULT: &str = include_str!("../assets/agent-skills/coordination.md");
const SUPERVISOR_DEFAULT: &str = include_str!("../assets/agent-skills/supervisor.md");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Source {
Embedded,
AgentsStandard,
User,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum SkillFormat {
Standardized,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StandardizedSkillMetadata {
pub name: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compatibility: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillTemplate {
pub name: String,
pub content: String,
pub source: Source,
pub format: SkillFormat,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<StandardizedSkillMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_paths: Option<Vec<PathBuf>>,
}
#[derive(Debug, thiserror::Error)]
pub enum SkillError {
#[error("unknown skill '{name}' — no embedded default or user override exists")]
UnknownSkill {
name: String,
},
#[error("skill '{name}' validation failed: {reason}")]
ValidationError {
name: String,
reason: String,
},
#[error("cannot read skill directory at '{}' — check directory permissions", path.display())]
DirectoryReadError {
path: PathBuf,
source: std::io::Error,
},
#[error("cannot read user override skill file at '{}' — check file permissions", path.display())]
UserOverrideRead {
path: PathBuf,
source: std::io::Error,
},
}
fn embedded_default(skill_name: &str) -> Option<&'static str> {
match skill_name {
"coordination" => Some(COORDINATION_DEFAULT),
"supervisor" => Some(SUPERVISOR_DEFAULT),
_ => None,
}
}
pub fn resolve(skill_name: &str) -> Result<SkillTemplate, SkillError> {
resolve_with_config_dir(skill_name, None)
}
fn try_load_standardized_skill(
skill_name: &str,
config_dir_override: Option<&Path>,
) -> Result<Option<SkillTemplate>, SkillError> {
if let Some(config_dir) = config_dir_override
&& let Some(skill) = try_load_user_override(skill_name, config_dir)?
{
return Ok(Some(skill));
}
try_load_from_agents_dir(skill_name)
}
fn try_load_user_override(
skill_name: &str,
config_dir: &Path,
) -> Result<Option<SkillTemplate>, SkillError> {
let skill_dir = config_dir
.join("git-paw")
.join("agent-skills")
.join(skill_name);
if skill_dir.is_dir() {
let skill_md_path = skill_dir.join("SKILL.md");
if skill_md_path.exists() {
return load_skill_from_directory(&skill_dir, skill_name, Source::User);
}
}
Ok(None)
}
fn try_load_from_agents_dir(skill_name: &str) -> Result<Option<SkillTemplate>, SkillError> {
let Ok(mut current_dir) = std::env::current_dir() else {
return Ok(None);
};
for _ in 0..5 {
let agents_dir = current_dir.join(".agents").join("skills").join(skill_name);
if agents_dir.is_dir() {
let skill_md_path = agents_dir.join("SKILL.md");
if skill_md_path.exists() {
return load_skill_from_directory(&agents_dir, skill_name, Source::AgentsStandard);
}
}
if !current_dir.pop() {
break;
}
}
Ok(None)
}
fn load_skill_from_directory(
skill_dir: &Path,
skill_name: &str,
source: Source,
) -> Result<Option<SkillTemplate>, SkillError> {
let skill_md_path = skill_dir.join("SKILL.md");
let content = match std::fs::read_to_string(&skill_md_path) {
Ok(content) => content,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source_err) => {
let error = match source {
Source::User => SkillError::UserOverrideRead {
path: skill_md_path.clone(),
source: source_err,
},
_ => SkillError::DirectoryReadError {
path: skill_dir.to_path_buf(),
source: source_err,
},
};
return Err(error);
}
};
let (metadata, content_without_frontmatter) = parse_standardized_metadata(&content)?;
let mut resource_paths = Vec::new();
for subdir in ["scripts", "references", "assets"] {
let subdir_path = skill_dir.join(subdir);
if subdir_path.exists() && subdir_path.is_dir() {
resource_paths.push(subdir_path);
}
}
Ok(Some(SkillTemplate {
name: skill_name.to_string(),
content: content_without_frontmatter,
source,
format: SkillFormat::Standardized,
metadata,
resource_paths: if resource_paths.is_empty() {
None
} else {
Some(resource_paths)
},
}))
}
fn parse_standardized_metadata(
content: &str,
) -> Result<(Option<StandardizedSkillMetadata>, String), SkillError> {
let lines: Vec<&str> = content.lines().collect();
if lines.len() < 2 || !lines[0].trim().starts_with("---") {
return Ok((None, content.to_string()));
}
let mut frontmatter_end = None;
for (i, line) in lines.iter().enumerate().skip(1) {
if line.trim().starts_with("---") {
frontmatter_end = Some(i);
break;
}
}
let Some(frontmatter_end) = frontmatter_end else {
return Ok((None, content.to_string())); };
let frontmatter_lines = &lines[1..frontmatter_end];
let frontmatter_yaml = frontmatter_lines.join("\n");
let metadata: StandardizedSkillMetadata = match serde_yaml::from_str(&frontmatter_yaml) {
Ok(meta) => meta,
Err(e) => {
return Err(SkillError::ValidationError {
name: "unknown".to_string(),
reason: format!("invalid YAML frontmatter: {e}"),
});
}
};
if metadata.name.is_empty() {
return Err(SkillError::ValidationError {
name: "unknown".to_string(),
reason: "missing required 'name' field in frontmatter".to_string(),
});
}
if metadata.description.is_empty() {
return Err(SkillError::ValidationError {
name: metadata.name.clone(),
reason: "missing required 'description' field in frontmatter".to_string(),
});
}
let content_without_frontmatter = lines[frontmatter_end + 1..].join("\n");
Ok((Some(metadata), content_without_frontmatter))
}
fn resolve_with_config_dir(
skill_name: &str,
config_dir: Option<&Path>,
) -> Result<SkillTemplate, SkillError> {
if let Some(skill) = try_load_standardized_skill(skill_name, config_dir)? {
return Ok(skill);
}
if let Some(content) = embedded_default(skill_name) {
let (metadata, content_without_frontmatter) = parse_standardized_metadata(content)?;
return Ok(SkillTemplate {
name: skill_name.to_string(),
content: content_without_frontmatter,
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata,
resource_paths: None,
});
}
Err(SkillError::UnknownSkill {
name: skill_name.to_string(),
})
}
fn slugify_branch(branch: &str) -> String {
crate::broker::messages::slugify_branch(branch)
}
pub fn build_boot_block(branch_id: &str, broker_url: &str) -> String {
let template = include_str!("../assets/boot-block-template.md");
let slugified_branch = slugify_branch(branch_id);
template
.replace("{{BRANCH_ID}}", &slugified_branch)
.replace("{{GIT_PAW_BROKER_URL}}", broker_url)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GateCommands<'a> {
pub test_command: Option<&'a str>,
pub lint_command: Option<&'a str>,
pub build_command: Option<&'a str>,
pub doc_build_command: Option<&'a str>,
pub spec_validate_command: Option<&'a str>,
pub fmt_check_command: Option<&'a str>,
pub security_audit_command: Option<&'a str>,
pub doc_tool_command: Option<&'a str>,
}
pub fn render(
template: &SkillTemplate,
branch: &str,
broker_url: &str,
project: &str,
gates: &GateCommands<'_>,
backends: &[crate::specs::SpecBackendKind],
) -> String {
const NOT_CONFIGURED: &str = "(not configured)";
let branch_id = slugify_branch(branch);
let allowlist_prose = render_dev_allowlist_preset();
let spec_doctrine = render_spec_path_doctrine(backends);
let mut output = template
.content
.replace("{{BRANCH_ID}}", &branch_id)
.replace("{{PROJECT_NAME}}", project)
.replace("{{GIT_PAW_BROKER_URL}}", broker_url)
.replace(
"{{TEST_COMMAND}}",
gates.test_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{LINT_COMMAND}}",
gates.lint_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{BUILD_COMMAND}}",
gates.build_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{DOC_BUILD_COMMAND}}",
gates.doc_build_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{SPEC_VALIDATE_COMMAND}}",
gates.spec_validate_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{FMT_CHECK_COMMAND}}",
gates.fmt_check_command.unwrap_or(NOT_CONFIGURED),
)
.replace(
"{{SECURITY_AUDIT_COMMAND}}",
gates.security_audit_command.unwrap_or(NOT_CONFIGURED),
)
.replace("{{DOC_TOOL_COMMAND}}", gates.doc_tool_command.unwrap_or(""))
.replace("{{DEV_ALLOWLIST_PRESET}}", &allowlist_prose)
.replace("{{SPEC_PATH_DOCTRINE}}", &spec_doctrine);
if let Some(metadata) = &template.metadata {
output = output
.replace("{{SKILL_NAME}}", &metadata.name)
.replace("{{SKILL_DESCRIPTION}}", &metadata.description);
}
let opsx_active = backends
.iter()
.any(|b| matches!(b, crate::specs::SpecBackendKind::OpenSpec));
output = render_opsx_regions(&output, opsx_active);
let mut start = 0;
while let Some(open) = output[start..].find("{{") {
let abs_open = start + open;
if let Some(close) = output[abs_open..].find("}}") {
let placeholder = &output[abs_open..abs_open + close + 2];
if placeholder != "{{CHANGE_ID}}" {
eprintln!(
"warning: unsubstituted placeholder {placeholder} in skill '{}'",
template.name
);
}
start = abs_open + close + 2;
} else {
break;
}
}
output
}
pub(crate) const OPSX_REGION_BEGIN: &str = "<!-- opsx-role-gating:begin -->";
pub(crate) const OPSX_REGION_END: &str = "<!-- opsx-role-gating:end -->";
#[must_use]
pub(crate) fn render_opsx_regions(input: &str, keep: bool) -> String {
let has_trailing_newline = input.ends_with('\n');
let mut kept: Vec<&str> = Vec::new();
let mut in_region = false;
for line in input.split('\n') {
let trimmed = line.trim();
if trimmed == OPSX_REGION_BEGIN {
in_region = true;
continue;
}
if trimmed == OPSX_REGION_END {
in_region = false;
continue;
}
if in_region && !keep {
continue;
}
kept.push(line);
}
let mut out = kept.join("\n");
if has_trailing_newline {
out.push('\n');
}
out
}
pub(crate) const SPEC_DOCTRINE_NO_BACKEND_SENTINEL: &str = "(no spec backend resolved for this session — see your project's documentation for where specs live.)";
#[must_use]
pub fn render_dev_allowlist_preset() -> String {
use crate::supervisor::dev_allowlist::DEV_ALLOWLIST_PRESET;
let mut groups: Vec<(String, Vec<String>)> = Vec::new();
for entry in DEV_ALLOWLIST_PRESET {
let (head, tail) = match entry.split_once(' ') {
Some((h, t)) => (h.to_string(), Some(t.to_string())),
None => (entry.to_string(), None),
};
if let Some(existing) = groups.iter_mut().find(|(h, _)| h == &head) {
if let Some(t) = tail {
existing.1.push(t);
}
} else {
groups.push((head, tail.into_iter().collect()));
}
}
let parts: Vec<String> = groups
.into_iter()
.map(|(head, members)| {
if members.is_empty() {
head
} else if members.len() == 1 {
format!("{head} {}", members[0])
} else {
format!("{head} ({})", members.join(", "))
}
})
.collect();
parts.join("; ")
}
#[must_use]
pub fn render_spec_path_doctrine(backends: &[crate::specs::SpecBackendKind]) -> String {
use crate::specs::SpecBackendKind;
let mut seen: Vec<SpecBackendKind> = Vec::new();
for b in backends {
if !seen.contains(b) {
seen.push(*b);
}
}
if seen.is_empty() {
return SPEC_DOCTRINE_NO_BACKEND_SENTINEL.to_string();
}
let per_backend = |kind: SpecBackendKind| -> &'static str {
match kind {
SpecBackendKind::OpenSpec => {
"OpenSpec specs live under `openspec/changes/<change-name>/{proposal,specs,tasks}.md` \
with archived deltas merged into `openspec/specs/`; run `openspec validate <change-name> --strict` \
to verify a change."
}
SpecBackendKind::SpecKit => {
"Spec Kit specs live under `.specify/specs/<feature>/{spec,plan,tasks}.md` \
and use the Spec Kit checklist convention; mark `- [ ]` tasks complete as each one lands."
}
SpecBackendKind::Markdown => {
"Markdown specs are flat `.md` files with `paw_status: pending` frontmatter; \
the format has no per-artifact workflow — the file itself is the contract."
}
}
};
if seen.len() == 1 {
per_backend(seen[0]).to_string()
} else {
let intro =
"This session spans multiple spec backends — apply the matching doctrine per spec:";
let sentences: Vec<String> = seen
.into_iter()
.map(|b| format!("- {}", per_backend(b)))
.collect();
format!("{intro}\n{}", sentences.join("\n"))
}
}
const GOVERNANCE_CANONICAL_NAMES: [&str; 5] =
["adr", "test_strategy", "security", "dod", "constitution"];
pub fn governance_section_paths(
adr: Option<&Path>,
test_strategy: Option<&Path>,
security: Option<&Path>,
dod: Option<&Path>,
constitution: Option<&Path>,
) -> String {
let bullets: [Option<&Path>; 5] = [adr, test_strategy, security, dod, constitution];
if bullets.iter().all(Option::is_none) {
return String::new();
}
let mut out = String::with_capacity(192);
out.push_str("## Governance documents\n");
out.push('\n');
out.push_str("The supervisor consults these documents during spec audit.\n");
out.push('\n');
for (name, path) in GOVERNANCE_CANONICAL_NAMES.iter().zip(bullets.iter()) {
if let Some(p) = path {
use std::fmt::Write as _;
let _ = writeln!(out, "- {name}: {}", p.display());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn embedded_coordination_is_reachable() {
let tmpl = resolve("coordination").expect("should resolve coordination");
assert_eq!(tmpl.source, Source::Embedded);
assert!(!tmpl.content.is_empty());
}
#[test]
fn embedded_coordination_contains_all_operations() {
let tmpl = resolve("coordination").unwrap();
assert!(tmpl.content.contains("agent.status"));
assert!(tmpl.content.contains("agent.artifact"));
assert!(tmpl.content.contains("agent.blocked"));
assert!(
tmpl.content
.contains("{{GIT_PAW_BROKER_URL}}/messages/{{BRANCH_ID}}")
);
}
#[test]
fn embedded_coordination_documents_supervisor_messages() {
let tmpl = resolve("coordination").unwrap();
assert!(tmpl.content.contains("agent.verified"));
assert!(tmpl.content.contains("agent.feedback"));
assert!(tmpl.content.contains("re-publish"));
}
#[test]
fn coordination_skill_documents_automatic_status_publishing() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("publishes your status automatically")
|| lowered.contains("status publishing is automatic")
|| lowered.contains("publishes status automatically"),
"coordination skill should indicate that agent.status publishing is automatic"
);
assert!(
!tmpl.content.contains("MUST publish agent.status"),
"coordination skill must not contain the legacy 'MUST publish agent.status' instruction"
);
}
#[test]
fn coordination_skill_contains_cherry_pick_instructions() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("git cherry-pick"),
"coordination skill should contain the literal 'git cherry-pick' command"
);
assert!(
tmpl.content.contains("Cherry-pick peer commits"),
"coordination skill should contain a 'Cherry-pick peer commits' heading"
);
}
#[test]
fn coordination_skill_teaches_main_advances_discipline() {
let tmpl = resolve("coordination").unwrap();
let content = &tmpl.content;
let idx = content
.find("When main advances")
.expect("coordination skill has a 'When main advances' subsection");
let section = &content[idx..];
let lowered = section.to_lowercase();
assert!(
section.contains("agent.advanced-main") && section.contains("/messages/{{BRANCH_ID}}"),
"subsection must name the event and its delivery on the normal /messages poll"
);
assert!(
lowered.contains("not auto-rebase")
|| lowered.contains("not trigger an automatic rebase"),
"subsection must contain an explicit do-not-auto-rebase rule"
);
assert!(
section.contains("git fetch origin")
&& section.contains("git log HEAD..origin/")
&& lowered.contains("decide"),
"subsection must document the fetch + inspect + decide flow"
);
assert!(
(lowered.contains("commit") || lowered.contains("stash"))
&& lowered.contains("before")
&& lowered.contains("rebase"),
"subsection must require a commit or stash before any rebase"
);
assert!(
lowered.contains("uncommitted"),
"subsection must include the concrete uncommitted-edits example"
);
}
#[test]
fn coordination_skill_contains_before_you_start_editing_heading() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("Before you start editing"),
"coordination skill should contain 'Before you start editing' heading"
);
}
#[test]
fn coordination_skill_contains_agent_intent_curl_example() {
let tmpl = resolve("coordination").unwrap();
let curl_pos = tmpl
.content
.find("agent.intent")
.expect("coordination skill should mention agent.intent");
let window_start = curl_pos.saturating_sub(200);
let window_end = (curl_pos + 800).min(tmpl.content.len());
let window = &tmpl.content[window_start..window_end];
assert!(
window.contains("curl"),
"agent.intent example should be a curl invocation"
);
assert!(
window.contains("\"files\""),
"agent.intent example should include the files field"
);
assert!(
window.contains("\"summary\""),
"agent.intent example should include the summary field"
);
assert!(
window.contains("\"valid_for_seconds\""),
"agent.intent example should include valid_for_seconds"
);
}
#[test]
fn coordination_skill_contains_while_youre_editing_heading() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("While you're editing"),
"coordination skill should contain 'While you're editing' heading"
);
}
#[test]
fn coordination_skill_instructs_republish_on_scope_growth() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("scope grows") || lowered.contains("scope grow"),
"coordination skill should instruct re-publishing when scope grows"
);
assert!(
lowered.contains("re-publish"),
"coordination skill should mention re-publishing the intent"
);
}
#[test]
fn coordination_skill_instructs_question_on_peer_intent_overlap() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("agent.question"),
"coordination skill should reference agent.question"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("overlap") || lowered.contains("overlapping"),
"coordination skill should call out overlap as the trigger for agent.question"
);
}
#[test]
fn coordination_skill_contains_must_not_anti_pattern_statements() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("must not"),
"coordination skill should contain explicit MUST NOT statements"
);
assert!(
lowered.contains("pairwise"),
"coordination skill should reject pairwise check-ins"
);
assert!(
lowered.contains("go-ahead") || lowered.contains("go ahead"),
"coordination skill should reject waiting for go-ahead"
);
assert!(
lowered.contains("broker silence") || lowered.contains("silence"),
"coordination skill should reject blocking on broker silence"
);
}
#[test]
fn supervisor_skill_contains_watch_peer_intents_section() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Watch peer intents"),
"supervisor skill should contain 'Watch peer intents' heading"
);
assert!(
tmpl.content.contains("agent.intent"),
"supervisor skill should mention agent.intent"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("not part of this release") || lowered.contains("conflict-detection"),
"supervisor skill should note that automatic conflict-warning logic is not part of this release"
);
}
#[test]
fn supervisor_skill_references_bundled_sweep_helper() {
let tmpl = resolve("supervisor").unwrap();
let required = [
".git-paw/scripts/sweep.sh snapshot",
".git-paw/scripts/sweep.sh capture",
".git-paw/scripts/sweep.sh approve",
".git-paw/scripts/sweep.sh verified",
".git-paw/scripts/sweep.sh feedback-gate",
];
for needle in required {
assert!(
tmpl.content.contains(needle),
"supervisor skill should reference {needle:?}; content does not"
);
}
assert!(
!tmpl.content.contains("for p in 2 3 4 5"),
"supervisor skill should not contain legacy `for p in 2 3 4 5` capture-pane loops"
);
}
#[test]
fn supervisor_skill_uses_repo_local_verify_scratch_dir() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains(".git-paw/tmp/verify-"),
"supervisor skill should name the repo-local verify scratch path .git-paw/tmp/verify-"
);
assert!(
tmpl.content.contains("git worktree add --detach"),
"supervisor skill should teach the `git worktree add --detach` verify recipe"
);
assert!(
!tmpl.content.contains("/tmp/paw-verify"),
"supervisor skill must not teach an OS-temp (/tmp/paw-verify) path for verify scratch"
);
}
#[test]
fn supervisor_skill_has_introspection_section_with_phase_taxonomy() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content
.contains("### Introspection: what to publish and when"),
"supervisor skill must include the introspection section"
);
for phase in [
"sweep",
"audit",
"merge",
"feedback",
"intent_watch",
"learnings",
"idle",
] {
assert!(
tmpl.content.contains(phase),
"the phase taxonomy must document the {phase:?} phase value"
);
}
for field in ["agents_checked", "audit_step", "intended_targets"] {
assert!(
tmpl.content.contains(field),
"the taxonomy must document the {field:?} detail field"
);
}
}
#[test]
fn supervisor_skill_audit_step_enumerates_five_gates() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("audit_step"),
"the audit phase must document the audit_step field"
);
for gate in ["tests", "regression", "spec", "docs", "security"] {
assert!(
tmpl.content.contains(gate),
"audit_step must enumerate the {gate:?} gate"
);
}
}
#[test]
fn supervisor_skill_documents_emission_cadence() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("phase transition"),
"cadence rules must require a status on every phase transition"
);
assert!(
lowered.contains("30 second") || tmpl.content.contains("~30 seconds"),
"cadence rules must document the ~30s rate-limit within a phase"
);
assert!(
lowered.contains("idle"),
"cadence rules must document the single-emit-on-idle rule"
);
}
#[test]
fn supervisor_skill_documents_checkpoint_phase() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("checkpoint"),
"the skill must document the checkpoint phase value"
);
assert!(
tmpl.content.contains("\"phase\":\"checkpoint\""),
"the checkpoint emission example must set phase = checkpoint"
);
}
#[test]
fn supervisor_skill_publishes_advanced_main_after_merge() {
let tmpl = resolve("supervisor").unwrap();
let content = &tmpl.content;
let merge_idx = content
.find("Merge orchestration")
.expect("supervisor skill has a Merge orchestration section");
let merge_section = &content[merge_idx..];
assert!(
merge_section.contains("agent.advanced-main"),
"the merge section must teach publishing an agent.advanced-main event"
);
assert!(
merge_section.contains("/publish") && merge_section.contains("new_main_sha"),
"the merge section must include a concrete curl /publish example carrying new_main_sha"
);
let lowered = merge_section.to_lowercase();
assert!(
lowered.contains("test command passes") || lowered.contains("after the merge succeeds"),
"the publish step must fire after a successful merge"
);
assert!(
merge_section.contains("$MAIN_BRANCH")
&& merge_section.contains("resolved default-branch"),
"the example must source `base` from the resolved default branch, not a hardcoded literal"
);
assert!(
!merge_section.contains("\"base\":\"main\"")
&& !merge_section.contains("\"base\": \"main\""),
"the example must not hardcode base as the literal \"main\""
);
}
#[test]
fn supervisor_skill_mandates_helper_and_forbids_inline_pane_loops() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Driving agent panes"),
"supervisor skill should contain a 'Driving agent panes' section"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("for p in") && lowered.contains("do tmux"),
"the section should name the forbidden `for p in ...; do tmux ...` loop shape"
);
assert!(
lowered.contains("simple_expansion"),
"the section should cite the simple_expansion permission gate as the reason"
);
}
#[test]
fn supervisor_skill_states_never_own_pane_rule() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("never") && lowered.contains("pane 0"),
"supervisor skill should state it must never send-keys to its own pane (pane 0)"
);
assert!(
lowered.contains("interrupt"),
"the never-own-pane rule should give the self-interrupt rationale"
);
}
#[test]
fn supervisor_skill_mandates_git_dash_c_and_forbids_cd() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("git -C"),
"supervisor skill should mandate `git -C <path>` for cross-worktree git"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("cd ") && lowered.contains("&& git"),
"the rule should name and forbid the `cd <path> && git` shape"
);
assert!(
lowered.contains("untrusted-hooks") || lowered.contains("untrusted hooks"),
"the rule should cite the untrusted-hooks warning"
);
assert!(
lowered.contains("wrong branch") || lowered.contains("wrong-branch"),
"the rule should cite the wrong-branch (cwd-leak) risk"
);
}
#[test]
fn supervisor_skill_states_commit_cadence_nudge() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("uncommitted") && lowered.contains("10"),
"supervisor skill should state the ~10-uncommitted-file commit-nudge threshold"
);
assert!(
lowered.contains("commit-cadence") || lowered.contains("commit cadence"),
"supervisor skill should label the commit-cadence nudge"
);
assert!(
tmpl.content.contains("feedback-gate"),
"the nudge should be a published agent.feedback (via the feedback-gate helper)"
);
}
#[test]
fn supervisor_skill_mandates_no_fail_fast_verification() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("never fail-fast") || lowered.contains("no-fail-fast"),
"testing gate must mandate running the whole suite (no fail-fast)"
);
assert!(
lowered.contains("guard test"),
"testing gate must name the environment guard-test failure mode"
);
assert!(
lowered.contains("incomplete, not a pass")
|| lowered.contains("not a pass unless every later suite"),
"testing gate must state that an early-aborted (guard-only) run is not a PASS"
);
}
#[test]
fn supervisor_skill_mandates_per_event_verification() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content
.contains("### Verify on each event, never batch"),
"supervisor skill must contain the 'Verify on each event, never batch' subsection"
);
assert!(
tmpl.content
.contains("MUST NOT** defer a ready verification"),
"subsection must state the no-batch rule in MUST-NOT terms"
);
assert!(
tmpl.content
.contains("MUST** start a branch's five-gate sweep"),
"subsection must state the per-event trigger in MUST terms"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("batching anti-pattern"),
"subsection must include a worked example of the batching anti-pattern"
);
assert!(
lowered.contains("still mid-task"),
"the worked example must name the wave-1 failure: waiting for a second agent to finish"
);
}
#[test]
fn supervisor_skill_permits_dependency_driven_deferral() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("only acceptable reason to defer is a genuine dependency"),
"subsection must state the genuine-dependency deferral exception"
);
assert!(
lowered.contains("state that dependency explicitly"),
"subsection must require stating the dependency explicitly when deferring"
);
}
#[test]
fn supervisor_skill_permits_concurrent_verification() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("per-branch verifications may run concurrently"),
"subsection must state per-branch verifications may run concurrently"
);
assert!(
lowered.contains("does **not** block starting agent b's verification"),
"subsection must state verifying agent A does not block verifying agent B"
);
}
#[test]
fn supervisor_skill_references_verify_now_nudge() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("supervisor.verify-now"),
"subsection must reference the broker's supervisor.verify-now nudge"
);
assert!(
tmpl.content.contains("verify_on_commit_nudge"),
"subsection must reference the [supervisor] verify_on_commit_nudge config gate"
);
}
#[test]
fn supervisor_skill_has_detecting_stuck_agents_section() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("### Detecting stuck agents"),
"supervisor skill must include a 'Detecting stuck agents' section"
);
assert!(
tmpl.content
.contains(".git-paw/scripts/sweep.sh detect-stuck"),
"the section must name the bundled detect-stuck helper command"
);
assert!(
tmpl.content.contains("stuck-on-prompt"),
"the section must document the stuck-on-prompt phase value"
);
assert!(
tmpl.content.contains("Pasted text #N"),
"the section must document the paste-buffer marker"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("dedup") && lowered.contains("prompt-shape"),
"the section must document the (agent_id, prompt-shape) dedup"
);
assert!(
tmpl.content
.contains("Do NOT hand-roll an inline-bash monitor"),
"the section must forbid inline-bash signature-dedup monitors"
);
assert!(
lowered.contains("eats repeat-pattern prompts"),
"the section must give the bug-9 rationale (signature dedup eats repeat-pattern prompts)"
);
}
#[test]
#[serial(directory_changes)]
fn standard_location_skill_loading() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("coordination");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: coordination\ndescription: Custom coordination skill\n---\n\ncustom skill content";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("coordination").expect("should resolve");
assert_eq!(tmpl.source, Source::AgentsStandard);
assert!(tmpl.content.contains("custom skill content"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn unknown_skill_returns_error() {
let result = resolve("nonexistent");
assert!(
matches!(result, Err(SkillError::UnknownSkill { ref name }) if name == "nonexistent"),
"expected UnknownSkill error, got {result:?}"
);
}
#[test]
fn branch_id_is_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "agent_id:\"{{BRANCH_ID}}\"".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/http-broker",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("feat-http-broker"));
assert!(!output.contains("{{BRANCH_ID}}"));
}
#[test]
fn broker_url_placeholder_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "curl {{GIT_PAW_BROKER_URL}}/status".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("http://127.0.0.1:9119/status"));
assert!(!output.contains("{{GIT_PAW_BROKER_URL}}"));
}
#[test]
fn slug_substitution_matches_slugify_branch() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "id={{BRANCH_ID}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"Feature/HTTP_Broker",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
let expected = slugify_branch("Feature/HTTP_Broker");
assert_eq!(output, format!("id={expected}"));
}
#[test]
fn render_is_deterministic() {
let tmpl = resolve("coordination").unwrap();
let a = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
let b = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert_eq!(a, b);
}
#[test]
#[serial(directory_changes)]
fn render_performs_no_io() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("coordination");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: coordination\ndescription: Test coordination skill\n---\n\nuser {{BRANCH_ID}}";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("coordination").unwrap();
assert_eq!(tmpl.source, Source::AgentsStandard);
std::fs::remove_dir_all(skill_dir).unwrap();
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("feat-x"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn unknown_placeholder_survives() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "url={{UNKNOWN_THING}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(
output.contains("{{UNKNOWN_THING}}"),
"unknown placeholder should survive in output"
);
}
#[test]
fn no_unknown_placeholders_after_render() {
let tmpl = resolve("coordination").unwrap();
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(
!output.contains("{{"),
"no double-curly placeholders should remain: {output}"
);
}
#[test]
fn embedded_supervisor_is_reachable() {
let tmpl = resolve("supervisor").expect("should resolve supervisor");
assert_eq!(tmpl.source, Source::Embedded);
assert!(!tmpl.content.is_empty());
}
#[test]
fn supervisor_skill_contains_role_definition() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("do NOT write code"));
}
#[test]
fn supervisor_skill_contains_broker_status() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("{{GIT_PAW_BROKER_URL}}/status"));
}
#[test]
fn supervisor_skill_contains_verified_and_feedback() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("agent.verified"));
assert!(tmpl.content.contains("agent.feedback"));
}
fn verified_curl_example_body(content: &str) -> &str {
let start = content
.find("\"type\":\"agent.verified\"")
.expect("supervisor skill should contain an agent.verified curl example");
let rest = &content[start..];
let end = rest
.find("}}'")
.expect("agent.verified curl example should terminate with the closing payload `}}'`");
&rest[..end + 3]
}
fn feedback_curl_example_body(content: &str) -> &str {
let start = content
.find("\"type\":\"agent.feedback\"")
.expect("supervisor skill should contain an agent.feedback curl example");
let rest = &content[start..];
let end = rest
.find("}}'")
.expect("agent.feedback curl example should terminate with the closing payload `}}'`");
&rest[..end + 3]
}
#[test]
fn supervisor_verified_example_uses_correct_payload_fields() {
let tmpl = resolve("supervisor").unwrap();
let example = verified_curl_example_body(&tmpl.content);
assert!(
example.contains("verified_by"),
"agent.verified example must use the `verified_by` payload field: {example}"
);
assert!(
example.contains("message"),
"agent.verified example must use the `message` payload field: {example}"
);
for wrong in ["\"target\"", "\"result\"", "\"notes\""] {
assert!(
!example.contains(wrong),
"agent.verified example must not contain the stale field key {wrong}: {example}"
);
}
}
#[test]
fn supervisor_feedback_example_uses_correct_payload_fields() {
let tmpl = resolve("supervisor").unwrap();
let example = feedback_curl_example_body(&tmpl.content);
assert!(
example.contains("\"from\""),
"agent.feedback example must use the `from` payload field: {example}"
);
assert!(
example.contains("\"errors\""),
"agent.feedback example must use the `errors` payload field: {example}"
);
assert!(
example.contains('['),
"agent.feedback example's errors field must be a JSON array (contains `[`): {example}"
);
assert!(
example.contains(']'),
"agent.feedback example's errors field must be a JSON array (contains `]`): {example}"
);
for wrong in ["\"target\"", "\"message\""] {
assert!(
!example.contains(wrong),
"agent.feedback example must not contain the stale field key {wrong}: {example}"
);
}
}
#[test]
fn supervisor_examples_clarify_recipient_vs_sender() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let verified_start = tmpl
.content
.find("### Publish verification outcome")
.expect("verified heading should be present");
let feedback_start = tmpl
.content
.find("### Publish feedback to a peer agent")
.expect("feedback heading should be present");
let verified_section = tmpl.content[verified_start..feedback_start].to_lowercase();
assert!(
verified_section.contains("recipient") && verified_section.contains("sender"),
"verified section should clarify recipient-vs-sender semantics, got: {verified_section}"
);
let after_feedback =
&tmpl.content[feedback_start + "### Publish feedback to a peer agent".len()..];
let feedback_end_rel = after_feedback
.find("\n### ")
.unwrap_or(after_feedback.len());
let feedback_section = after_feedback[..feedback_end_rel].to_lowercase();
assert!(
feedback_section.contains("recipient") && feedback_section.contains("sender"),
"feedback section should clarify recipient-vs-sender semantics, got: {feedback_section}"
);
assert!(lowered.contains("recipient"));
assert!(lowered.contains("sender"));
}
#[test]
fn supervisor_workflow_prose_drops_legacy_verified_fields() {
let tmpl = resolve("supervisor").unwrap();
let condensed: String = tmpl
.content
.chars()
.filter(|c| !c.is_whitespace())
.collect();
assert!(
!condensed.contains("result:\"pass\""),
"workflow prose must not reference `result:\"pass\"` as the verified payload"
);
assert!(
!condensed.contains("notes:\"\""),
"workflow prose must not reference `notes:\"\"` as the verified payload"
);
}
#[test]
fn supervisor_skill_contains_tmux_commands() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("tmux capture-pane"));
assert!(tmpl.content.contains("tmux send-keys"));
assert!(tmpl.content.contains("paw-{{PROJECT_NAME}}"));
}
#[test]
fn supervisor_skill_contains_spec_audit_procedure() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Spec Audit"),
"supervisor skill should contain Spec Audit section"
);
assert!(
tmpl.content.contains("{{SPEC_PATH_DOCTRINE}}"),
"v0.6.0+ supervisor template should embed the SPEC_PATH_DOCTRINE placeholder so spec layout is rendered per backend, not hardcoded"
);
assert!(
tmpl.content.contains("grep"),
"should instruct to grep for matching tests"
);
let rendered = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[crate::specs::SpecBackendKind::OpenSpec],
);
assert!(
rendered.contains("openspec/changes/"),
"OpenSpec-rendered supervisor skill should reference openspec/changes/ via the SPEC_PATH_DOCTRINE substitution"
);
}
#[test]
fn supervisor_skill_spec_audit_after_test_before_verified() {
let tmpl = resolve("supervisor").unwrap();
let test_pos = tmpl.content.find("Regression check").unwrap_or(0);
let audit_pos = tmpl.content.find("Spec Audit").unwrap_or(0);
let verify_pos = tmpl.content.find("Verify or feedback").unwrap_or(0);
assert!(
audit_pos > test_pos,
"spec audit should appear after test/regression check"
);
assert!(
audit_pos < verify_pos,
"spec audit should appear before verify/feedback"
);
}
#[test]
fn supervisor_skill_mentions_paste_buffer_recovery() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("paste-buffer") || lowered.contains("paste buffer"),
"supervisor skill should contain paste-buffer recovery sub-case"
);
}
#[test]
fn supervisor_skill_mentions_pasted_text_indicator() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Pasted text"),
"supervisor skill should mention the Claude Code 'Pasted text' indicator"
);
}
#[test]
fn supervisor_skill_paste_buffer_recovery_uses_tmux() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.to_lowercase()
.find("paste-buffer recovery")
.or_else(|| tmpl.content.to_lowercase().find("paste buffer recovery"))
.expect("paste-buffer recovery sub-case heading should be present");
let window_end = (start + 2200).min(tmpl.content.len());
let window = &tmpl.content[start..window_end];
assert!(
window.contains(".git-paw/scripts/sweep.sh capture")
|| window.contains("tmux capture-pane"),
"paste-buffer recovery should reference a pane-capture command (sweep.sh capture or tmux capture-pane)"
);
assert!(
window.contains("tmux send-keys"),
"paste-buffer recovery should reference tmux send-keys for the Enter recovery"
);
assert!(
window.contains("Enter"),
"paste-buffer recovery should specify Enter as the recovery keystroke"
);
}
#[test]
fn supervisor_skill_mentions_launch_time_sweep() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("launch-time pane sweep")
|| lowered.contains("launch time pane sweep")
|| lowered.contains("launch sweep"),
"supervisor skill should contain a launch-time pane sweep heading"
);
}
#[test]
fn supervisor_skill_launch_sweep_lists_four_pane_categories() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("launch-time pane sweep")
.or_else(|| lowered.find("launch sweep"))
.expect("launch-time pane sweep heading should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("paste-buffer") || window.contains("paste buffer"),
"launch sweep should enumerate paste-buffer category"
);
assert!(
window.contains("permission prompt"),
"launch sweep should enumerate permission-prompt category"
);
assert!(
window.contains("working"),
"launch sweep should enumerate working category"
);
assert!(
window.contains("idle"),
"launch sweep should enumerate idle category"
);
}
#[test]
fn supervisor_skill_launch_sweep_references_down_enter_keystroke() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("launch-time pane sweep")
.or_else(|| lowered.find("launch sweep"))
.expect("launch-time pane sweep heading should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("down"),
"launch sweep should reference the Down keystroke for selecting 'don't ask again'"
);
assert!(
window.contains("enter"),
"launch sweep should reference the Enter keystroke for confirming approval"
);
assert!(
window.contains("don't ask again") || window.contains("don't ask"),
"launch sweep should mention the 'don't ask again' approval option"
);
}
#[test]
fn supervisor_skill_paste_buffer_recovery_is_safe_by_default() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("paste-buffer recovery")
.or_else(|| lowered.find("paste buffer recovery"))
.expect("paste-buffer recovery sub-case heading should be present");
let window_end = (start + 2200).min(lowered.len());
let window = &lowered[start..window_end];
let safe_phrasing = window.contains("safe-by-default")
|| window.contains("safe by default")
|| window.contains("no-op")
|| window.contains("no harm");
assert!(
safe_phrasing,
"paste-buffer recovery should explicitly note the Enter is safe-by-default / no-op / no harm"
);
}
#[test]
fn supervisor_skill_contains_governance_verification() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Governance verification"),
"supervisor skill should contain 'Governance verification' heading"
);
}
#[test]
fn supervisor_skill_governance_is_substep_of_spec_audit() {
let tmpl = resolve("supervisor").unwrap();
let audit_pos = tmpl
.content
.find("### Spec Audit Procedure")
.expect("Spec Audit Procedure heading must exist");
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification must exist");
let conflict_pos = tmpl
.content
.find("### Conflict detection")
.unwrap_or(tmpl.content.len());
assert!(
gov_pos > audit_pos,
"Governance verification should appear inside Spec Audit Procedure (after its heading)"
);
assert!(
gov_pos < conflict_pos,
"Governance verification should appear before the next top-level subsection (Conflict detection), keeping it inside Spec Audit Procedure"
);
assert!(
!tmpl.content.contains("step 7.5"),
"Governance verification must not be framed as a separate 'step 7.5' flow step"
);
}
#[test]
fn supervisor_skill_governance_examples_cover_all_five_docs() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
for needle in &["DoD", "ADR", "Security", "Test strategy", "Constitution"] {
assert!(
section.contains(needle),
"governance section should mention `{needle}` as a per-doc example, got:\n{section}"
);
}
}
#[test]
fn supervisor_skill_governance_findings_via_agent_feedback() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
assert!(
section.contains("agent.feedback"),
"governance section must state that findings flow through `agent.feedback`"
);
}
#[test]
fn supervisor_skill_no_governance_gate_tag() {
let tmpl = resolve("supervisor").unwrap();
assert!(
!tmpl.content.contains("[governance-gate:"),
"supervisor skill must not contain the dropped `[governance-gate:<doc>]` tag prefix"
);
}
#[test]
fn supervisor_skill_no_governance_gates_table() {
let tmpl = resolve("supervisor").unwrap();
assert!(
!tmpl.content.contains("[governance.gates]"),
"supervisor skill must not reference the dropped `[governance.gates]` table"
);
}
#[test]
fn supervisor_skill_no_gating_language() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl
.content
.to_lowercase()
.replace("opsx-role-gating", "")
.replace("role-gating", "")
.replace("role_gating", "");
assert!(
!lowered.contains("gating"),
"supervisor skill must not use the language of 'gating' (outside the opsx role-gating feature name)"
);
assert!(
!lowered.contains("blocking on governance failures"),
"supervisor skill must not use the language of 'blocking on governance failures'"
);
}
#[test]
fn supervisor_skill_governance_missing_doc_handling() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("missing"),
"governance section should describe missing-doc handling"
);
assert!(
section.contains("agent.feedback"),
"missing-doc handling should reference `agent.feedback` errors list"
);
}
#[test]
fn supervisor_skill_governance_missing_doc_is_not_distinct_failure_type() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("not a distinct failure")
|| lowered.contains("not a separate failure")
|| lowered.contains("treat it as a finding"),
"governance section must state that missing files are findings, not a distinct failure type; got:\n{section}"
);
}
#[test]
fn supervisor_skill_governance_states_activation_condition() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("skip"),
"governance section must instruct the supervisor to skip the sub-step when the boot prompt has no `## Governance documents` section; got:\n{section}"
);
assert!(
section.contains("## Governance documents"),
"governance section must reference the boot-prompt heading explicitly as its activation condition; got:\n{section}"
);
}
#[test]
fn supervisor_skill_governance_examples_state_they_are_illustrative() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("illustrative") || lowered.contains("not exhaustive"),
"governance section must state per-doc examples are illustrative / not exhaustive rubrics; got:\n{section}"
);
}
#[test]
fn supervisor_skill_governance_states_judgment_per_project_conventions() {
let tmpl = resolve("supervisor").unwrap();
let gov_pos = tmpl
.content
.find("Governance verification")
.expect("Governance verification section must exist");
let after = &tmpl.content[gov_pos..];
let end = after.find("\n### ").unwrap_or(after.len());
let section = &after[..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("judgment"),
"governance section must state the supervisor applies judgment; got:\n{section}"
);
assert!(
lowered.contains("convention") || lowered.contains("project"),
"governance section must reference the project's conventions / process when describing judgment; got:\n{section}"
);
}
fn stream_timeout_section(content: &str) -> &str {
let start = content
.find("### Stream-timeout recovery")
.expect("supervisor skill must contain the Stream-timeout recovery section");
let after = &content[start..];
let body_offset = "### Stream-timeout recovery".len();
let end = after[body_offset..]
.find("\n### ")
.map_or(after.len(), |i| body_offset + i);
&after[..end]
}
#[test]
fn supervisor_skill_stream_timeout_section_has_four_ordered_pieces() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
let error_shape = section
.find("error-shape recognition")
.expect("subsection 1 must name error-shape recognition");
let checkpoint = section
.find("pre-action checkpoint")
.expect("subsection 2 must name the pre-action checkpoint");
let replay = section
.find("replay-missing-publishes")
.expect("subsection 3 must name replay-missing-publishes");
let confirmation = section
.find("Confirmation rule")
.expect("subsection 4 must name the Confirmation rule");
assert!(
error_shape < checkpoint && checkpoint < replay && replay < confirmation,
"the four pieces must appear in recovery order: error-shape recognition, \
pre-action checkpoint, replay-missing-publishes, confirmation rule"
);
}
#[test]
fn supervisor_skill_stream_timeout_names_two_generic_symptoms() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
let lowered = section.to_lowercase();
assert!(
lowered.contains("mid-stream cutoff"),
"error-shape subsection must name the mid-stream cutoff symptom"
);
assert!(
lowered.contains("transport error") || lowered.contains("stream error"),
"error-shape subsection must name a transport-error / stream-error symptom"
);
}
#[test]
fn supervisor_skill_stream_timeout_documents_checkpoint_shape() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
assert!(
section.contains("agent.status"),
"checkpoint subsection must show an agent.status publish"
);
assert!(
section.contains("\"status\":\"checkpoint\"")
|| section.contains("status: \"checkpoint\""),
"checkpoint subsection must show status: \"checkpoint\""
);
assert!(
section.contains("summary"),
"checkpoint subsection must show a summary enumerating intended targets"
);
}
#[test]
fn supervisor_skill_stream_timeout_checkpoint_only_for_multi_publish() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
let lowered = section.to_lowercase();
assert!(
lowered.contains("more than one"),
"checkpoint subsection must state it applies only to iterations with \
more than one intended downstream publish"
);
assert!(
lowered.contains("not to every sweep") || lowered.contains("not every sweep"),
"checkpoint subsection must clarify it does not apply to every sweep"
);
}
#[test]
fn supervisor_skill_stream_timeout_documents_replay_loop() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
assert!(
section.contains("/messages/"),
"replay subsection must show polling the target's /messages/ stream"
);
let lowered = section.to_lowercase();
assert!(
lowered.contains("since=") || lowered.contains("checkpoint timestamp"),
"replay subsection must poll since the checkpoint timestamp"
);
assert!(
lowered.contains("re-publish"),
"replay subsection must re-publish the missing record"
);
assert!(
lowered.contains("idempotent"),
"replay subsection must state the replay is idempotent so duplicates are safe"
);
assert!(
lowered.contains("for each"),
"replay subsection must show a per-target loop"
);
}
#[test]
fn supervisor_skill_stream_timeout_confirmation_rule_is_prominent() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
assert!(
section.contains("**Never advance to the next sub-action"),
"confirmation rule must be marked prominently with bold (`**`) formatting"
);
let lowered = section.to_lowercase();
assert!(
lowered.contains("timed out mid-write") || lowered.contains("may have timed out"),
"confirmation rule must pair with a one-sentence rationale referencing stream-timeout risk"
);
}
#[test]
fn supervisor_skill_stream_timeout_names_recovery_learning_record() {
let tmpl = resolve("supervisor").unwrap();
let section = stream_timeout_section(&tmpl.content);
assert!(
section.contains("recovery_cycles"),
"replay subsection must name the recovery_cycles learning category"
);
assert!(
section.contains("agent.learning"),
"replay subsection must state the recovery emits an agent.learning record"
);
for field in [
"checkpoint_id",
"intended_targets",
"replayed_targets",
"skipped_targets",
] {
assert!(
section.contains(field),
"recovery learning body must document the `{field}` field"
);
}
}
#[test]
fn dev_allowlist_preset_renders_every_constant_entry() {
use crate::supervisor::dev_allowlist::DEV_ALLOWLIST_PRESET;
let prose = render_dev_allowlist_preset();
for entry in DEV_ALLOWLIST_PRESET {
let (head, tail) = match entry.split_once(' ') {
Some((h, t)) => (h, Some(t)),
None => (*entry, None),
};
assert!(
prose.contains(head),
"rendered preset must contain head word `{head}` from entry `{entry}`; got:\n{prose}"
);
if let Some(t) = tail {
assert!(
prose.contains(t),
"rendered preset must contain tail `{t}` from entry `{entry}`; got:\n{prose}"
);
}
}
}
#[test]
fn dev_allowlist_preset_groups_by_first_word() {
let prose = render_dev_allowlist_preset();
let cargo_groups = prose.matches("cargo (").count();
assert_eq!(
cargo_groups, 1,
"multi-entry prefixes must collapse into a single grouped clause; got {cargo_groups} occurrences of `cargo (` in:\n{prose}"
);
let git_groups = prose.matches("git (").count();
assert_eq!(
git_groups, 1,
"multi-entry git prefix must collapse into a single grouped clause; got {git_groups} occurrences of `git (` in:\n{prose}"
);
}
#[test]
fn dev_allowlist_preset_preserves_single_word_entries() {
let prose = render_dev_allowlist_preset();
for bare in ["just", "find", "grep"] {
assert!(
prose.contains(bare),
"bare single-word entry `{bare}` should appear verbatim in:\n{prose}"
);
}
}
#[test]
fn spec_doctrine_empty_backends_renders_sentinel() {
let out = render_spec_path_doctrine(&[]);
assert!(
out.contains("no spec backend"),
"empty backend slice should render the sentinel; got: {out}"
);
}
#[test]
fn spec_doctrine_openspec_references_openspec_paths_and_workflow() {
use crate::specs::SpecBackendKind;
let out = render_spec_path_doctrine(&[SpecBackendKind::OpenSpec]);
assert!(
out.contains("openspec/changes/"),
"OpenSpec doctrine should name the openspec/changes/ path; got: {out}"
);
assert!(
out.contains("openspec validate"),
"OpenSpec doctrine should reference the openspec validate workflow; got: {out}"
);
}
#[test]
fn spec_doctrine_speckit_references_specify_paths_and_checklist() {
use crate::specs::SpecBackendKind;
let out = render_spec_path_doctrine(&[SpecBackendKind::SpecKit]);
assert!(
out.contains(".specify/specs/"),
"Spec Kit doctrine should name the .specify/specs/ path; got: {out}"
);
assert!(
out.to_lowercase().contains("checklist"),
"Spec Kit doctrine should reference the checklist convention; got: {out}"
);
}
#[test]
fn spec_doctrine_markdown_references_paw_status_frontmatter() {
use crate::specs::SpecBackendKind;
let out = render_spec_path_doctrine(&[SpecBackendKind::Markdown]);
assert!(
out.contains("paw_status: pending"),
"Markdown doctrine should reference paw_status: pending; got: {out}"
);
}
#[test]
fn spec_doctrine_multi_backend_lists_each_present_backend() {
use crate::specs::SpecBackendKind;
let out = render_spec_path_doctrine(&[
SpecBackendKind::OpenSpec,
SpecBackendKind::SpecKit,
SpecBackendKind::Markdown,
]);
assert!(
out.contains("openspec/changes/"),
"multi-backend doctrine should mention OpenSpec; got:\n{out}"
);
assert!(
out.contains(".specify/specs/"),
"multi-backend doctrine should mention Spec Kit; got:\n{out}"
);
assert!(
out.contains("paw_status: pending"),
"multi-backend doctrine should mention Markdown; got:\n{out}"
);
assert!(
out.contains("spans multiple"),
"multi-backend doctrine should introduce the multi-backend session shape; got:\n{out}"
);
}
#[test]
fn spec_doctrine_dedupes_repeated_backends() {
use crate::specs::SpecBackendKind;
let out = render_spec_path_doctrine(&[
SpecBackendKind::OpenSpec,
SpecBackendKind::OpenSpec,
SpecBackendKind::OpenSpec,
]);
assert!(
!out.contains("spans multiple"),
"duplicate backends must collapse to the single-backend shape; got:\n{out}"
);
}
#[test]
fn render_doc_tool_command_substitutes_from_gates() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{DOC_TOOL_COMMAND}} for API docs.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
doc_tool_command: Some("sphinx-build -W docs docs/_build"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert_eq!(output, "Run sphinx-build -W docs docs/_build for API docs.");
assert!(!output.contains("{{DOC_TOOL_COMMAND}}"));
}
#[test]
fn render_doc_tool_command_empty_when_unset() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "API doc tool: `{{DOC_TOOL_COMMAND}}`".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert_eq!(output, "API doc tool: ``");
assert!(!output.contains("(not configured)"));
}
#[test]
fn render_dev_allowlist_preset_placeholder_substitutes() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Allowed: {{DEV_ALLOWLIST_PRESET}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(
output.contains("cargo (build"),
"rendered placeholder should embed the grouped preset prose; got:\n{output}"
);
assert!(!output.contains("{{DEV_ALLOWLIST_PRESET}}"));
}
#[test]
fn render_spec_path_doctrine_placeholder_substitutes_per_backend() {
use crate::specs::SpecBackendKind;
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Spec layout: {{SPEC_PATH_DOCTRINE}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let openspec_output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[SpecBackendKind::OpenSpec],
);
assert!(openspec_output.contains("openspec/changes/"));
assert!(!openspec_output.contains("{{SPEC_PATH_DOCTRINE}}"));
let speckit_output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[SpecBackendKind::SpecKit],
);
assert!(speckit_output.contains(".specify/specs/"));
}
#[test]
fn render_spec_path_doctrine_empty_renders_sentinel() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "{{SPEC_PATH_DOCTRINE}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("no spec backend"));
}
#[test]
fn governance_section_empty_when_all_paths_none() {
let out = governance_section_paths(None, None, None, None, None);
assert!(
out.is_empty(),
"governance_section_paths should return empty string when all paths are None, got: {out:?}"
);
}
#[test]
fn governance_section_one_path_only_dod() {
let dod = Path::new("docs/dod.md");
let out = governance_section_paths(None, None, None, Some(dod), None);
assert!(
out.contains("## Governance documents"),
"section should include the canonical heading, got:\n{out}"
);
assert!(
out.contains("- dod: docs/dod.md"),
"section should include the dod bullet, got:\n{out}"
);
for unset in [
"- adr:",
"- test_strategy:",
"- security:",
"- constitution:",
] {
assert!(
!out.contains(unset),
"section should not mention `{unset}` when its path is None, got:\n{out}"
);
}
}
#[test]
fn governance_section_lists_all_five_in_canonical_order() {
let adr = Path::new("docs/adr/");
let test_strategy = Path::new("docs/test-strategy.md");
let security = Path::new("docs/security.md");
let dod = Path::new("docs/dod.md");
let constitution = Path::new("docs/constitution.md");
let out = governance_section_paths(
Some(adr),
Some(test_strategy),
Some(security),
Some(dod),
Some(constitution),
);
let order = [
"- adr: docs/adr/",
"- test_strategy: docs/test-strategy.md",
"- security: docs/security.md",
"- dod: docs/dod.md",
"- constitution: docs/constitution.md",
];
let mut last_pos = 0usize;
for bullet in order {
let idx = out
.find(bullet)
.unwrap_or_else(|| panic!("bullet `{bullet}` not found in:\n{out}"));
assert!(
idx >= last_pos,
"bullets must appear in canonical adr -> test_strategy -> security -> dod -> constitution order; `{bullet}` came before a previous bullet in:\n{out}"
);
last_pos = idx;
}
}
#[test]
fn governance_section_has_no_gates_text() {
let out = governance_section_paths(
Some(Path::new("docs/adr/")),
Some(Path::new("docs/test-strategy.md")),
Some(Path::new("docs/security.md")),
Some(Path::new("docs/dod.md")),
Some(Path::new("docs/constitution.md")),
);
let lowered = out.to_lowercase();
assert!(
!lowered.contains("gated docs"),
"section should not contain a 'Gated docs' line, got:\n{out}"
);
assert!(
!lowered.contains("governance gates"),
"section should not contain a 'Governance gates' sub-section, got:\n{out}"
);
assert!(
!out.contains("[governance.gates]"),
"section should not reference the dropped [governance.gates] table, got:\n{out}"
);
assert!(
!out.contains("[governance-gate:"),
"section should not introduce the dropped [governance-gate:<doc>] tag, got:\n{out}"
);
}
#[test]
fn governance_section_has_preamble_line() {
let out = governance_section_paths(None, None, None, Some(Path::new("docs/dod.md")), None);
let preamble = "The supervisor consults these documents during spec audit.";
assert!(
out.contains(preamble),
"section should include the preamble line; got:\n{out}"
);
let heading_pos = out.find("## Governance documents").unwrap();
let preamble_pos = out.find(preamble).unwrap();
let bullet_pos = out.find("- dod:").unwrap();
assert!(
heading_pos < preamble_pos && preamble_pos < bullet_pos,
"section layout should be heading -> preamble -> bullets; got:\n{out}"
);
}
#[test]
fn project_name_is_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "session=paw-{{PROJECT_NAME}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"my-app",
&GateCommands::default(),
&[],
);
assert!(output.contains("paw-my-app"));
assert!(!output.contains("{{PROJECT_NAME}}"));
}
#[test]
fn branch_id_and_project_name_both_substituted() {
let tmpl = SkillTemplate {
name: "test".into(),
content: "agent={{BRANCH_ID}} session=paw-{{PROJECT_NAME}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"feat/http-broker",
"url",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("feat-http-broker"));
assert!(output.contains("paw-git-paw"));
assert!(!output.contains("{{BRANCH_ID}}"));
assert!(!output.contains("{{PROJECT_NAME}}"));
}
#[test]
#[serial(directory_changes)]
fn standardized_skill_format_is_detected() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("test-standardized");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: test-standardized\ndescription: A test standardized skill\n---\n\nThis is the skill content with {{BRANCH_ID}} placeholder.";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("test-standardized").expect("should resolve");
assert_eq!(tmpl.format, SkillFormat::Standardized);
assert!(tmpl.content.contains("This is the skill content"));
assert!(tmpl.content.contains("{{BRANCH_ID}}"));
assert!(tmpl.metadata.is_some());
let metadata = tmpl.metadata.as_ref().unwrap();
assert_eq!(metadata.name, "test-standardized");
assert_eq!(metadata.description, "A test standardized skill");
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn standardized_skill_with_resources_loads_paths() {
let dir = tempfile::tempdir().unwrap();
let skills_parent_dir = dir.path().join("git-paw").join("agent-skills");
let specific_skill_dir = skills_parent_dir.join("test-with-resources");
std::fs::create_dir_all(&specific_skill_dir).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("scripts")).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("references")).unwrap();
std::fs::create_dir_all(specific_skill_dir.join("assets")).unwrap();
let skill_md_content = "---\nname: test-with-resources\ndescription: Skill with resources\n---\n\nMain content here.";
std::fs::write(specific_skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let tmpl = resolve_with_config_dir("test-with-resources", Some(dir.path()))
.expect("should resolve");
assert_eq!(tmpl.format, SkillFormat::Standardized);
assert!(tmpl.resource_paths.is_some());
let resource_paths = tmpl.resource_paths.as_ref().unwrap();
assert_eq!(resource_paths.len(), 3);
assert!(resource_paths.iter().any(|p| p.ends_with("scripts")));
assert!(resource_paths.iter().any(|p| p.ends_with("references")));
assert!(resource_paths.iter().any(|p| p.ends_with("assets")));
}
#[test]
#[serial(directory_changes)]
fn standard_location_loading() {
let temp_dir = tempfile::tempdir().unwrap();
let project_dir = temp_dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let standard_skill_dir = project_dir
.join(".agents")
.join("skills")
.join("test-skill");
std::fs::create_dir_all(&standard_skill_dir).unwrap();
let standard_content = "---\nname: test-skill\ndescription: Standard location skill\n---\n\nContent from .agents/skills/";
std::fs::write(standard_skill_dir.join("SKILL.md"), standard_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let tmpl = resolve("test-skill").expect("should resolve");
assert_eq!(tmpl.source, Source::AgentsStandard);
assert!(tmpl.content.contains("Content from .agents/skills/"));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn standardized_skill_metadata_placeholders_are_substituted() {
let metadata = StandardizedSkillMetadata {
name: "test-skill".to_string(),
description: "Test description".to_string(),
license: None,
compatibility: None,
metadata: None,
};
let tmpl = SkillTemplate {
name: "test".into(),
content: "Name: {{SKILL_NAME}}, Desc: {{SKILL_DESCRIPTION}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: Some(metadata),
resource_paths: None,
};
let output = render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert!(output.contains("Name: test-skill, Desc: Test description"));
assert!(!output.contains("{{SKILL_NAME}}"));
assert!(!output.contains("{{SKILL_DESCRIPTION}}"));
}
#[test]
fn test_command_placeholder_substitutes_when_set() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run `{{TEST_COMMAND}}` after each merge.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands {
test_command: Some("just check"),
..Default::default()
},
&[],
);
assert_eq!(output, "Run `just check` after each merge.");
assert!(!output.contains("{{TEST_COMMAND}}"));
}
#[test]
fn test_command_placeholder_falls_back_when_unset() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Baseline: {{TEST_COMMAND}}".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
assert_eq!(output, "Baseline: (not configured)");
assert!(!output.contains("{{TEST_COMMAND}}"));
}
#[test]
fn supervisor_template_no_unsubstituted_placeholders_when_test_command_set() {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands {
test_command: Some("just check"),
..Default::default()
},
&[],
);
assert!(
!output.contains("{{TEST_COMMAND}}"),
"supervisor template still contains a literal {{TEST_COMMAND}} after render"
);
let remaining: String = output.replace("{{CHANGE_ID}}", "").chars().collect();
assert!(
!remaining.contains("{{"),
"supervisor template has unsubstituted {{...}} placeholder (other than {{CHANGE_ID}}) after render"
);
}
fn render_with_gates_uniform(template: &str, value: Option<&str>) -> String {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: template.into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
test_command: value,
lint_command: value,
build_command: value,
doc_build_command: value,
spec_validate_command: value,
fmt_check_command: value,
security_audit_command: value,
doc_tool_command: value,
};
render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
)
}
#[test]
fn render_test_command_placeholder_substitutes_from_config() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{TEST_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
test_command: Some("just check"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(
output.contains("Run just check."),
"expected 'Run just check.' in: {output}"
);
}
#[test]
fn render_test_command_placeholder_none_renders_not_configured() {
let output = render_with_gates_uniform("Run {{TEST_COMMAND}}.", None);
assert!(
output.contains("Run (not configured)."),
"expected 'Run (not configured).' in: {output}"
);
}
#[test]
fn render_lint_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{LINT_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
lint_command: Some("cargo clippy -- -D warnings"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(
output.contains("Run cargo clippy -- -D warnings."),
"expected substitution in: {output}"
);
let none_output = render_with_gates_uniform("Run {{LINT_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"expected '(not configured)' fallback in: {none_output}"
);
}
#[test]
fn render_build_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{BUILD_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
build_command: Some("cargo build"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(output.contains("Run cargo build."), "got: {output}");
let none_output = render_with_gates_uniform("Run {{BUILD_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"got: {none_output}"
);
}
#[test]
fn render_doc_build_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{DOC_BUILD_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
doc_build_command: Some("mdbook build docs/"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(output.contains("Run mdbook build docs/."), "got: {output}");
let none_output = render_with_gates_uniform("Run {{DOC_BUILD_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"got: {none_output}"
);
}
#[test]
fn render_spec_validate_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{SPEC_VALIDATE_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
spec_validate_command: Some("openspec validate {{CHANGE_ID}} --strict"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(
output.contains("Run openspec validate {{CHANGE_ID}} --strict."),
"got: {output}"
);
let none_output = render_with_gates_uniform("Run {{SPEC_VALIDATE_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"got: {none_output}"
);
}
#[test]
fn render_fmt_check_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{FMT_CHECK_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
fmt_check_command: Some("cargo fmt --check"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(output.contains("Run cargo fmt --check."), "got: {output}");
let none_output = render_with_gates_uniform("Run {{FMT_CHECK_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"got: {none_output}"
);
}
#[test]
fn render_security_audit_command_placeholder_substitutes_and_none_fallback() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{SECURITY_AUDIT_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
security_audit_command: Some("cargo audit"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(output.contains("Run cargo audit."), "got: {output}");
let none_output = render_with_gates_uniform("Run {{SECURITY_AUDIT_COMMAND}}.", None);
assert!(
none_output.contains("Run (not configured)."),
"got: {none_output}"
);
}
#[test]
fn supervisor_skill_renders_with_all_six_gate_placeholders_set() {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
let gates = GateCommands {
test_command: Some("CMD-TEST"),
lint_command: Some("CMD-LINT"),
build_command: Some("CMD-BUILD"),
doc_build_command: Some("CMD-DOC"),
spec_validate_command: Some("CMD-SPEC"),
fmt_check_command: Some("CMD-FMT"),
security_audit_command: Some("CMD-SEC"),
doc_tool_command: Some("CMD-DOCTOOL"),
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
for needle in [
"CMD-TEST",
"CMD-LINT",
"CMD-BUILD",
"CMD-DOC",
"CMD-SPEC",
"CMD-FMT",
"CMD-SEC",
] {
assert!(
output.contains(needle),
"rendered supervisor skill should contain '{needle}'; not found"
);
}
}
#[test]
fn supervisor_skill_renders_not_configured_in_each_gate_when_none() {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
);
let testing_start = output.find("**Testing**").expect("Testing gate present");
let testing_end = output[testing_start..]
.find("**Regression analysis**")
.map(|p| testing_start + p)
.expect("Regression follows Testing");
let testing_section = &output[testing_start..testing_end];
assert!(
testing_section.contains("(not configured)"),
"Testing gate should render '(not configured)' when gate fields are None; got:\n{testing_section}"
);
let spec_start = output.find("**Spec audit**").expect("Spec audit present");
let spec_end = output[spec_start..]
.find("**Doc audit**")
.map(|p| spec_start + p)
.expect("Doc audit follows Spec audit");
let spec_section = &output[spec_start..spec_end];
assert!(
spec_section.contains("(not configured)"),
"Spec audit gate should render '(not configured)' when None; got:\n{spec_section}"
);
let doc_start = output.find("**Doc audit**").expect("Doc audit present");
let doc_end = output[doc_start..]
.find("**Security audit**")
.map(|p| doc_start + p)
.expect("Security audit follows Doc audit");
let doc_section = &output[doc_start..doc_end];
assert!(
doc_section.contains("(not configured)"),
"Doc audit gate should render '(not configured)' when None; got:\n{doc_section}"
);
let security_start = output
.find("**Security audit**")
.expect("Security audit present");
let security_end = output[security_start..]
.find("**Verify or feedback**")
.map(|p| security_start + p)
.expect("Verify-or-feedback follows Security audit");
let security_section = &output[security_start..security_end];
assert!(
security_section.contains("(not configured)"),
"Security audit gate should render '(not configured)' when None; got:\n{security_section}"
);
}
#[test]
fn supervisor_template_gate_prose_has_no_hardcoded_git_paw_commands() {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
let content = &tmpl.content;
let start = content
.find("Steps 4-7 below are the **five first-class verification gates**")
.expect("five-gate intro present");
let end = content
.find("### Spec Audit Procedure")
.expect("Spec Audit Procedure heading present");
let gate_prose = &content[start..end];
for needle in [
"just check",
"cargo test",
"cargo clippy",
"cargo audit",
"cargo fmt --check",
"mdbook build",
] {
if needle == "cargo test"
&& (gate_prose.contains("[testing] cargo test failed")
|| gate_prose.contains("testing \"cargo test failed"))
{
let cleaned = gate_prose.replace("cargo test failed", "<failure>");
assert!(
!cleaned.contains("cargo test"),
"gate prose must not contain hardcoded 'cargo test' outside the §7 example"
);
continue;
}
assert!(
!gate_prose.contains(needle),
"gate prose must not contain hardcoded '{needle}'; replace with the matching placeholder"
);
}
}
#[test]
fn render_change_id_placeholder_passes_through() {
let tmpl = SkillTemplate {
name: "supervisor".into(),
content: "Run {{SPEC_VALIDATE_COMMAND}}.".into(),
source: Source::Embedded,
format: SkillFormat::Standardized,
metadata: None,
resource_paths: None,
};
let gates = GateCommands {
spec_validate_command: Some("openspec validate {{CHANGE_ID}} --strict"),
..Default::default()
};
let output = render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&gates,
&[],
);
assert!(
output.contains("Run openspec validate {{CHANGE_ID}} --strict."),
"outer placeholder substituted but inner {{CHANGE_ID}} preserved; got: {output}"
);
assert!(
output.contains("{{CHANGE_ID}}"),
"{{CHANGE_ID}} must survive verbatim (not substituted at render time); got: {output}"
);
}
#[test]
fn invalid_standardized_skill_frontmatter_returns_error() {
let dir = tempfile::tempdir().unwrap();
let project_dir = dir.path().join("my-project");
std::fs::create_dir_all(&project_dir).unwrap();
let skill_dir = project_dir
.join(".agents")
.join("skills")
.join("invalid-skill");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_md_content = "---\nname: invalid-skill\n---\n\nContent here.";
std::fs::write(skill_dir.join("SKILL.md"), skill_md_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&project_dir).unwrap();
let result = resolve("invalid-skill");
assert!(matches!(result, Err(SkillError::ValidationError { .. })));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn skill_template_is_cloneable() {
let tmpl = resolve("coordination").unwrap();
let cloned = tmpl.clone();
assert_eq!(tmpl.name, cloned.name);
assert_eq!(tmpl.content, cloned.content);
assert_eq!(tmpl.source, cloned.source);
}
#[test]
fn boot_block_contains_all_four_essential_events() {
let block = build_boot_block("feat/errors", "http://localhost:9119");
assert!(
block.contains("### 1. REGISTER"),
"Missing REGISTER section"
);
assert!(block.contains("### 2. DONE"), "Missing DONE section");
assert!(block.contains("### 3. BLOCKED"), "Missing BLOCKED section");
assert!(
block.contains("### 4. QUESTION"),
"Missing QUESTION section"
);
}
#[test]
fn boot_block_substitutes_branch_id_placeholder() {
let block = build_boot_block("Feature/HTTP_Broker", "http://localhost:9119");
assert!(
block.contains("feature-http_broker"),
"Branch ID not properly slugified"
);
assert!(
!block.contains("{{BRANCH_ID}}"),
"BRANCH_ID placeholder not substituted"
);
}
#[test]
fn boot_block_substitutes_broker_url_placeholder() {
let block = build_boot_block("feat/x", "http://127.0.0.1:9119");
assert!(
block.contains("http://127.0.0.1:9119/publish"),
"Broker URL not substituted"
);
assert!(
!block.contains("{{GIT_PAW_BROKER_URL}}"),
"GIT_PAW_BROKER_URL placeholder not substituted"
);
}
#[test]
fn boot_block_contains_paste_handling_instructions() {
let block = build_boot_block("feat/x", "http://localhost:9119");
assert!(
block.contains("PASTE HANDLING"),
"Missing paste handling section"
);
assert!(
block.contains("additional Enter key"),
"Missing Enter key instruction"
);
assert!(
block.contains("[Pasted text #N]"),
"Missing paste text reference"
);
}
#[test]
fn boot_block_question_section_emphasizes_waiting() {
let block = build_boot_block("feat/x", "http://localhost:9119");
assert!(
block.contains("DO NOT CONTINUE UNTIL YOU RECEIVE AN ANSWER!"),
"Missing wait emphasis"
);
assert!(
block.contains("WAIT for the answer before continuing"),
"Missing wait instruction"
);
}
#[test]
fn boot_block_is_deterministic() {
let a = build_boot_block("feat/x", "http://localhost:9119");
let b = build_boot_block("feat/x", "http://localhost:9119");
assert_eq!(a, b, "Boot block generation should be deterministic");
}
#[test]
fn boot_block_handles_complex_branch_names() {
let block = build_boot_block("fix/topological-cycle-fallback", "http://localhost:9119");
assert!(
block.contains("fix-topological-cycle-fallback"),
"Complex branch name not properly slugified"
);
}
#[test]
fn boot_block_contains_pre_expanded_curl_commands() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
assert!(
block.contains("curl -s -X POST http://127.0.0.1:9119/publish"),
"Curl commands not pre-expanded"
);
assert!(
block.contains("\"agent_id\":\"feat-test\""),
"Agent ID not substituted in curl commands"
);
}
fn done_section_body(block: &str) -> String {
let start = block
.find("### 2. DONE")
.expect("rendered boot block should contain the DONE section heading");
let end = block
.find("### 3. BLOCKED")
.expect("rendered boot block should contain the BLOCKED section heading");
block[start..end].to_string()
}
#[test]
fn boot_block_done_section_leads_with_commit_instruction() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
let done_body = done_section_body(&block);
let commit_idx = done_body
.find("commit your work")
.or_else(|| done_body.find("git commit"))
.expect("DONE section should lead with a commit-first instruction");
let manual_done_idx = done_body
.find("\"status\":\"done\"")
.expect("DONE section should still contain the manual done curl as a fallback");
assert!(
commit_idx < manual_done_idx,
"commit-first instruction (byte {commit_idx}) must appear before the manual done curl (byte {manual_done_idx})"
);
}
#[test]
fn boot_block_done_section_names_committed_status_published_by_hook() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
let done_body = done_section_body(&block);
assert!(
done_body.contains("status: \"committed\"")
|| done_body.contains("status:\"committed\""),
"DONE section should name the `status: \"committed\"` event published by the hook"
);
assert!(
done_body.contains("post-commit hook"),
"DONE section should mention the post-commit hook that publishes on the agent's behalf"
);
}
#[test]
fn boot_block_done_section_scopes_manual_done_to_code_less_tasks() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
let done_body = done_section_body(&block);
let hits = ["docs-only", "planning", "exploration"]
.iter()
.filter(|needle| done_body.contains(*needle))
.count();
assert!(
hits >= 2,
"DONE section should enumerate at least two code-less task examples \
(docs-only / planning / exploration); only {hits} present"
);
}
#[test]
fn boot_block_done_section_warns_against_manual_done_with_uncommitted_changes() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
let done_body = done_section_body(&block);
assert!(
done_body.contains("uncommitted"),
"DONE section should warn about uncommitted changes"
);
assert!(
done_body.contains("manual `done`") || done_body.contains("manual done"),
"DONE section warning should reference manual `done`"
);
assert!(
done_body.contains("**WARNING") || done_body.contains("**DO NOT"),
"DONE section warning should be emphasised with bold markers (**...**)"
);
}
#[test]
fn boot_block_done_section_retains_manual_done_curl() {
let block = build_boot_block("feat/test", "http://127.0.0.1:9119");
let done_body = done_section_body(&block);
assert!(
done_body.contains("curl -s -X POST http://127.0.0.1:9119/publish"),
"DONE section should retain the pre-expanded broker curl"
);
assert!(
done_body.contains("\"type\":\"agent.artifact\""),
"DONE section curl should publish an agent.artifact event"
);
assert!(
done_body.contains("\"status\":\"done\""),
"DONE section curl should still publish status: done as the manual fallback"
);
assert!(
done_body.contains("\"exports\":[]"),
"DONE section curl should retain the exports field"
);
assert!(
done_body.contains("\"modified_files\":[]"),
"DONE section curl should retain the modified_files field"
);
}
#[test]
fn supervisor_skill_contains_conflict_detector_tag() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("[conflict-detector]"),
"supervisor skill should reference the [conflict-detector] tag"
);
}
#[test]
fn supervisor_skill_documents_broker_side_detection() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("auto-detect") || lowered.contains("auto-emit"),
"skill should mention auto-detection/auto-emission by the broker"
);
assert!(
lowered.contains("forward conflict"),
"skill should mention forward conflict"
);
assert!(
lowered.contains("in-flight conflict"),
"skill should mention in-flight conflict"
);
assert!(
lowered.contains("ownership violation"),
"skill should mention ownership violation"
);
}
#[test]
fn supervisor_skill_removes_v04_manual_conflict_detection() {
let tmpl = resolve("supervisor").unwrap();
assert!(
!tmpl
.content
.contains("Compare the `modified_files` arrays from every `agent.artifact` event"),
"supervisor skill should no longer contain the v0.4 manual conflict-comparison instructions"
);
}
#[test]
fn supervisor_skill_mentions_agent_intent() {
let tmpl = resolve("supervisor").unwrap();
assert!(tmpl.content.contains("agent.intent"));
assert!(
tmpl.content.contains("Watch peer intents")
|| tmpl
.content
.contains("Watch peer intents and broker-side conflict detection"),
"skill should contain a 'Watch peer intents' heading"
);
}
#[test]
fn supervisor_skill_focuses_on_question_escalations() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("agent.question")
&& (lowered.contains("escalation") || lowered.contains("escalat")),
"skill should direct the supervisor agent at agent.question escalations"
);
assert!(
lowered.contains("do not") && lowered.contains("manually"),
"skill should tell the supervisor not to duplicate by manual comparison"
);
}
#[test]
fn embedded_coordination_mentions_spec_kit_consolidated_worktrees() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("Spec Kit")
&& (tmpl.content.contains("consolidated") || tmpl.content.contains("phase/")),
"coordination skill should mention Spec Kit consolidated worktrees"
);
}
#[test]
fn embedded_coordination_instructs_sequential_work_and_writeback() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("sequential") || tmpl.content.contains("Sequential"),
"should instruct sequential execution"
);
assert!(
tmpl.content.contains("`- [x]`") || tmpl.content.contains("- [x]"),
"should mention - [x] writeback"
);
assert!(
tmpl.content.contains("tasks.md"),
"should reference tasks.md as writeback target"
);
}
#[test]
fn embedded_coordination_states_agent_done_timing_for_consolidated() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("agent.done"),
"should mention agent.done"
);
let lower = tmpl.content.to_lowercase();
assert!(
lower.contains("every task")
|| lower.contains("all listed tasks")
|| lower.contains("all tasks"),
"should tie agent.done to completion of all listed tasks"
);
}
#[test]
fn embedded_coordination_clarifies_p_worktrees_follow_standard_pattern() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("[P]") || tmpl.content.contains("task/"),
"should distinguish [P] / task/ worktrees from consolidated ones"
);
assert!(
tmpl.content.contains("standard"),
"should reference the standard before/while-editing pattern"
);
}
#[test]
fn supervisor_skill_has_user_input_section() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("When the user types in your pane"),
"supervisor skill should include the 'When the user types in your pane' section"
);
}
#[test]
fn supervisor_skill_user_input_uses_agent_feedback_for_directives() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("When the user types in your pane")
.expect("user-input section heading present");
let window = &tmpl.content[start..];
assert!(
window.contains("agent.feedback"),
"user-input directives section should reference agent.feedback"
);
}
#[test]
fn supervisor_skill_user_input_uses_agent_question_for_judgment_calls() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("When the user types in your pane")
.expect("user-input section heading present");
let window = &tmpl.content[start..];
assert!(
window.contains("agent.question"),
"user-input judgment-call section should reference agent.question"
);
}
#[test]
fn supervisor_skill_user_input_states_loop_continues() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("When the user types in your pane")
.expect("user-input section heading present");
let window = &tmpl.content[start..];
assert!(
window.to_lowercase().contains("autonomous"),
"user-input section should state the autonomous loop continues alongside user input"
);
}
#[test]
fn supervisor_skill_has_merge_orchestration_section() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("Merge orchestration"),
"supervisor skill should include the 'Merge orchestration' section"
);
}
#[test]
fn supervisor_skill_merge_uses_ff_only() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Merge orchestration")
.expect("merge orchestration section present");
let window = &tmpl.content[start..];
assert!(
window.contains("git merge --ff-only"),
"merge orchestration should specify git merge --ff-only"
);
}
#[test]
fn supervisor_skill_merge_reverts_via_reset_hard() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Merge orchestration")
.expect("merge orchestration section present");
let window = &tmpl.content[start..];
assert!(
window.contains("git reset --hard"),
"merge orchestration should describe regression revert via git reset --hard"
);
}
#[test]
fn supervisor_skill_merge_cycle_uses_agent_question() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Merge orchestration")
.expect("merge orchestration section present");
let window = &tmpl.content[start..];
assert!(
window.contains("agent.question") && window.to_lowercase().contains("cycle"),
"merge orchestration cycle handling should publish agent.question"
);
}
#[test]
fn supervisor_skill_merge_publishes_final_status_summary() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Merge orchestration")
.expect("merge orchestration section present");
let window = &tmpl.content[start..];
assert!(
window.contains("agent.status") && window.to_lowercase().contains("summary"),
"merge orchestration should end with a final agent.status summary"
);
}
#[test]
fn coordination_skill_documents_slugify_terminology() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("agent_id"),
"coordination skill should mention the agent_id identifier form"
);
assert!(
tmpl.content.contains("slugify_branch"),
"coordination skill should name slugify_branch as the canonical conversion"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("references & terminology")
|| lowered.contains("references and terminology")
|| lowered.contains("terminology"),
"coordination skill should contain a references/terminology heading"
);
}
#[test]
fn coordination_skill_documents_stash_hygiene() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("git stash list"),
"stash-hygiene section should reference `git stash list`"
);
assert!(
tmpl.content.contains("git stash show -p"),
"stash-hygiene section should reference `git stash show -p`"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("stash hygiene") || lowered.contains("stash safety"),
"coordination skill should contain a stash-hygiene heading"
);
assert!(
lowered.contains("pop only") || lowered.contains("only pop"),
"coordination skill should instruct agents to pop only their own stashes"
);
}
#[test]
fn supervisor_skill_documents_main_side_intent() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("supervisor publishes agent.intent")
|| lowered.contains("publish intent")
|| lowered.contains("main-side work"),
"supervisor skill should contain a heading naming supervisor-side intent publishing"
);
let start = tmpl
.content
.find("Supervisor publishes agent.intent")
.expect("supervisor-publishes-intent heading present");
let window = &tmpl.content[start..];
assert!(
window.contains("agent.intent"),
"section should mention agent.intent"
);
assert!(
window.contains("\"supervisor\""),
"section should show agent_id = \"supervisor\" in the example"
);
assert!(
window.contains("\"files\"")
&& window.contains("\"summary\"")
&& window.contains("\"valid_for_seconds\""),
"section should include a curl example with files, summary, valid_for_seconds"
);
}
#[test]
fn supervisor_skill_documents_tmux_send_keys_alongside_feedback() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Send the answer to the agent pane too")
.expect("drift-34 subsection should be present");
let next_heading = tmpl.content[start + 1..]
.find("\n### ")
.map_or(tmpl.content.len(), |off| start + 1 + off);
let section = &tmpl.content[start..next_heading];
assert!(
section.contains("tmux send-keys"),
"section should contain `tmux send-keys`"
);
assert!(
section.contains("agent.feedback"),
"section should reference agent.feedback in the same section"
);
let lowered_section = section.to_lowercase();
assert!(
lowered_section.contains("do not poll") || lowered_section.contains("don't poll"),
"section should state the rationale (agents do not poll their inbox)"
);
}
#[test]
fn coordination_skill_documents_working_heartbeat() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("working heartbeat") || lowered.contains("heartbeat"),
"coordination skill should contain a working-heartbeat heading"
);
assert!(
tmpl.content.contains("every 5 tool uses"),
"coordination skill should state the cadence as 'every 5 tool uses'"
);
assert!(
tmpl.content.contains("agent.status"),
"heartbeat reuses the agent.status shape — substring should be present"
);
let start = tmpl
.content
.find("Working heartbeat")
.expect("Working heartbeat heading present");
let next_heading = tmpl.content[start + 1..]
.find("\n### ")
.map_or(tmpl.content.len(), |off| start + 1 + off);
let section = &tmpl.content[start..next_heading].to_lowercase();
assert!(
section.contains("filesystem watcher") || section.contains("watcher"),
"heartbeat section should explain why the filesystem watcher is insufficient"
);
}
#[test]
fn supervisor_skill_documents_accept_edits_audit() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("accept-edits commits") || lowered.contains("accept edits"),
"supervisor skill should contain an accept-edits audit heading"
);
assert!(
tmpl.content.contains("modified_files"),
"audit section should reference the modified_files payload field"
);
let start = tmpl
.content
.find("Verify accept-edits commits before merge")
.expect("accept-edits audit heading present");
let next_heading = tmpl.content[start + 1..]
.find("\n### ")
.map_or(tmpl.content.len(), |off| start + 1 + off);
let section_lower = tmpl.content[start..next_heading].to_lowercase();
assert!(
section_lower.contains("out-of-scope"),
"audit section should call out 'out-of-scope' edits"
);
assert!(
section_lower.contains("shall not be silently")
|| section_lower.contains("not be silently auto-approved")
|| section_lower.contains("silently auto-approved"),
"audit section should forbid silent auto-approval"
);
}
#[test]
fn coordination_skill_describes_slugify_rule() {
let tmpl = resolve("coordination").unwrap();
let start = tmpl
.content
.find("slugify_branch")
.expect("slugify_branch should be named in the references section");
let next_heading = tmpl.content[start + 1..]
.find("\n### ")
.map_or(tmpl.content.len(), |off| start + 1 + off);
let section_lower = tmpl.content[start..next_heading].to_lowercase();
assert!(
section_lower.contains("lowercase"),
"slugify rule should mention lowercase step"
);
assert!(
tmpl.content[start..next_heading].contains("[a-z0-9_]"),
"slugify rule should describe the allowed char class"
);
assert!(
(section_lower.contains("fallback") || section_lower.contains("fall back"))
&& section_lower.contains("agent"),
"slugify rule should describe the empty-fallback to `agent`"
);
}
fn rendered_supervisor() -> String {
let tmpl = resolve("supervisor").expect("supervisor skill resolves");
render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
)
}
fn rendered_coordination() -> String {
let tmpl = resolve("coordination").expect("coordination skill resolves");
render(
&tmpl,
"feat/x",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
&[],
)
}
#[test]
fn supervisor_skill_paste_buffer_framing_is_lenient() {
let content = rendered_supervisor();
let lowered = content.to_lowercase();
assert!(
lowered.contains("even if"),
"supervisor skill should frame recovery as attempted even when indicator absent; got:\n{content}"
);
assert!(
lowered.contains("judgment"),
"supervisor skill should describe applying judgment; got:\n{content}"
);
assert!(
lowered.contains("long buffered text"),
"supervisor skill should mention the long-buffered-text heuristic; got:\n{content}"
);
}
#[test]
fn coordination_skill_rejects_pairwise_overcoordination() {
let content = rendered_coordination();
assert!(
content.contains("pairwise"),
"coordination skill should name `pairwise` under a MUST-NOT clause; got:\n{content}"
);
let lowered = content.to_lowercase();
assert!(
lowered.contains("explicit go-ahead"),
"coordination skill should reject waiting for an explicit go-ahead; got:\n{content}"
);
assert!(
lowered.contains("broker silence") || lowered.contains("block on broker silence"),
"coordination skill should reject blocking on broker silence; got:\n{content}"
);
}
#[test]
fn coordination_skill_verified_and_feedback_substrings_independent() {
let content = rendered_coordination();
let verified_anchor = "- **`agent.verified`**";
let feedback_anchor = "- **`agent.feedback`**";
assert!(
content.contains(verified_anchor),
"coordination skill should anchor `agent.verified` in its own bullet; got:\n{content}"
);
assert!(
content.contains(feedback_anchor),
"coordination skill should anchor `agent.feedback` in its own bullet; got:\n{content}"
);
let v = content.find(verified_anchor).unwrap();
let f = content.find(feedback_anchor).unwrap();
let between = if v < f {
&content[v..f]
} else {
&content[f..v]
};
assert!(
between.contains('\n'),
"the verified and feedback bullets must be on separate lines; got slice:\n{between}"
);
}
#[test]
fn supervisor_skill_governance_after_spec_audit_before_verified() {
let content = rendered_supervisor();
let spec_audit = content
.find("Spec Audit Procedure")
.expect("Spec Audit Procedure heading present in supervisor skill");
let governance = content
.find("Governance verification")
.expect("Governance verification heading present in supervisor skill");
let verified_after = content[governance..]
.find("agent.verified")
.map(|o| governance + o)
.expect("agent.verified mention after Governance verification");
assert!(
spec_audit < governance,
"Spec Audit Procedure should appear before Governance verification \
(spec_audit={spec_audit}, governance={governance})"
);
assert!(
governance < verified_after,
"Governance verification should appear before the next agent.verified \
publish step (governance={governance}, verified_after={verified_after})"
);
}
#[test]
fn coordination_skill_consolidated_agent_done_timing() {
let content = rendered_coordination();
let start = content
.find("consolidated worktree")
.or_else(|| content.find("Consolidated worktree"))
.expect("coordination skill should have a consolidated-worktree section");
let section = &content[start..];
let lowered = section.to_lowercase();
assert!(
lowered.contains("agent.done") || lowered.contains("agent.artifact"),
"consolidated-worktree section should describe agent.done timing; got:\n{section}"
);
assert!(
section.contains("- [x]"),
"consolidated-worktree section should require every task to show - [x]; got:\n{section}"
);
assert!(
lowered.contains("every task") || lowered.contains("every"),
"consolidated-worktree section should make the rule cover every task; got:\n{section}"
);
}
#[test]
fn supervisor_skill_cross_references_agent_intent_flow() {
let tmpl = resolve("supervisor").unwrap();
let start = tmpl
.content
.find("Supervisor publishes agent.intent")
.expect("supervisor-publishes-intent heading present");
let next_heading = tmpl.content[start + 1..]
.find("\n### ")
.map_or(tmpl.content.len(), |off| start + 1 + off);
let section = &tmpl.content[start..next_heading];
assert!(
section.contains("Before you start editing"),
"supervisor-publishes-intent section should cross-reference the agent-side \
`Before you start editing` heading"
);
assert!(
section.contains("coordination.md"),
"cross-reference should name the coordination skill file"
);
}
fn render_supervisor() -> String {
let tmpl = resolve("supervisor").expect("resolve supervisor template");
render(
&tmpl,
"supervisor",
"http://127.0.0.1:9119",
"git-paw",
&GateCommands {
test_command: Some("just check"),
..Default::default()
},
&[],
)
}
#[test]
fn supervisor_skill_self_register_curl_omits_cli_field() {
let rendered = render_supervisor();
let start = rendered
.find("Bootstrap")
.expect("Bootstrap section heading present");
let next = rendered[start..]
.find("### Poll session status and messages")
.map_or(rendered.len(), |p| start + p);
let section = &rendered[start..next];
assert!(
section.contains("agent.status"),
"bootstrap section must publish agent.status; got:\n{section}"
);
assert!(
section.contains("\"agent_id\":\"supervisor\""),
"bootstrap curl must use agent_id=\"supervisor\"; got:\n{section}"
);
assert!(
!section.contains("\"cli\""),
"bootstrap payload must NOT self-report a cli field (git-paw pre-fills it); got:\n{section}"
);
}
#[test]
fn supervisor_skill_self_register_is_first_action() {
let rendered = render_supervisor();
let pos_bootstrap = rendered
.find("Bootstrap")
.expect("Bootstrap heading present");
let section_end = rendered[pos_bootstrap..]
.find("### Poll session status and messages")
.map_or(rendered.len(), |p| pos_bootstrap + p);
let section = &rendered[pos_bootstrap..section_end];
let lower = section.to_lowercase();
assert!(
lower.contains("first action") || lower.contains("very first"),
"bootstrap section must state this is the agent's first action; got:\n{section}"
);
}
#[test]
fn supervisor_skill_watch_mentions_per_iteration_sweep() {
let rendered = render_supervisor();
let start = rendered
.find("**Watch**")
.expect("Watch step heading present");
let end = rendered[start..]
.find("Stall detection")
.map_or(rendered.len(), |p| start + p);
let section = &rendered[start..end];
let lower = section.to_lowercase();
assert!(
lower.contains("every iteration")
|| lower.contains("every monitoring")
|| lower.contains("each monitoring")
|| lower.contains("each iteration"),
"Watch section must mention per-iteration sweeping; got:\n{section}"
);
}
#[test]
fn supervisor_skill_rules_bullet_mentions_routine_absorption() {
let rendered = render_supervisor();
let start = rendered.find("### Rules").expect("Rules section present");
let end = rendered[start..]
.find("### Auto-approve permission prompts")
.map_or(rendered.len(), |p| start + p);
let section = &rendered[start..end];
let lower = section.to_lowercase();
assert!(
lower.contains("absorb routine approval") || lower.contains("rubber-stamp"),
"Rules must include the routine-approval absorption framing; got:\n{section}"
);
let mut family_hits = 0;
for family in ["cargo (", "git (", "mdbook", "openspec (", "just"] {
if section.contains(family) {
family_hits += 1;
}
}
assert!(
family_hits >= 3,
"Rules bullet must enumerate at least 3 routine families; only {family_hits} found in:\n{section}",
);
}
#[test]
fn supervisor_skill_rules_bullet_enumerates_escalation_cases() {
let rendered = render_supervisor();
let start = rendered.find("### Rules").expect("Rules section present");
let end = rendered[start..]
.find("### Auto-approve permission prompts")
.map_or(rendered.len(), |p| start + p);
let section = &rendered[start..end];
let lower = section.to_lowercase();
let mut hits = 0;
for case in [
"cross-agent conflict",
"destructive",
"scope",
"spec decisions",
"novel",
] {
if lower.contains(case) {
hits += 1;
}
}
assert!(
hits >= 2,
"Rules bullet must enumerate at least 2 escalation cases; only {hits} found in:\n{section}",
);
}
#[test]
fn supervisor_skill_contains_every_iteration_phrase() {
let rendered = render_supervisor();
let lower = rendered.to_lowercase();
assert!(
lower.contains("every iteration") || lower.contains("every monitoring"),
"skill must contain 'every iteration' or 'every monitoring' phrasing somewhere",
);
}
#[test]
fn supervisor_skill_enumerates_five_gates_in_order() {
let rendered = render_supervisor();
let pos = |needle: &str| {
rendered
.find(needle)
.unwrap_or_else(|| panic!("gate '{needle}' not found in supervisor skill"))
};
let pos_testing = pos("**Testing**");
let pos_regression = pos("**Regression analysis**");
let pos_spec = pos("**Spec audit**");
let pos_doc = pos("**Doc audit**");
let pos_security = pos("**Security audit**");
assert!(
pos_testing < pos_regression
&& pos_regression < pos_spec
&& pos_spec < pos_doc
&& pos_doc < pos_security,
"five gates must appear in order Testing < Regression < Spec < Doc < Security; \
got positions Testing={pos_testing} Regression={pos_regression} \
Spec={pos_spec} Doc={pos_doc} Security={pos_security}",
);
}
#[test]
fn supervisor_skill_verified_message_enumerates_five_gates() {
let rendered = render_supervisor();
let verify_start = rendered
.find("**Verify or feedback**")
.expect("Verify or feedback step present");
let window = &rendered[verify_start..];
let lower = window.to_lowercase();
for needle in [
"testing",
"regression",
"spec audit",
"doc audit",
"security audit",
] {
assert!(
lower.contains(needle),
"§7 Verify-or-feedback must mention '{needle}'; got window:\n{window}",
);
}
}
#[test]
fn supervisor_skill_feedback_example_uses_gate_name_prefixes() {
let rendered = render_supervisor();
let verify_start = rendered
.find("**Verify or feedback**")
.expect("Verify or feedback step present");
let end = rendered[verify_start..]
.find("\n### ")
.map_or(rendered.len(), |p| verify_start + p);
let window = &rendered[verify_start..end];
let mut hits = 0;
for (bracketed, helper_arg) in [
("[testing]", " testing "),
("[regression]", " regression "),
("[spec audit]", " \"spec audit\" "),
("[doc audit]", " \"doc audit\" "),
("[security audit]", " \"security audit\" "),
] {
if window.contains(bracketed)
|| window.contains(&format!("feedback-gate __FILL_IN_AGENT_ID__{helper_arg}"))
{
hits += 1;
}
}
assert!(
hits >= 3,
"§7 agent.feedback example must show at least 3 gates (bracketed or helper-arg); \
only {hits} found in:\n{window}",
);
}
#[test]
fn supervisor_skill_doc_audit_enumerates_surfaces() {
let rendered = render_supervisor();
let start = rendered
.find("**Doc audit**")
.expect("Doc audit gate present");
let end = rendered[start..]
.find("**Security audit**")
.map(|p| start + p)
.expect("Security audit follows Doc audit");
let section = &rendered[start..end];
let mut hits = 0;
for surface in [
"user-guide",
"README.md",
"AGENTS.md",
"--help",
"doc_tool_command",
] {
if section.contains(surface) {
hits += 1;
}
}
assert!(
hits >= 4,
"Doc audit must enumerate at least 4 of 5 doc-surface categories; only {hits} found in:\n{section}",
);
}
#[test]
fn supervisor_skill_security_audit_enumerates_owasp_categories() {
let rendered = render_supervisor();
let start = rendered
.find("**Security audit**")
.expect("Security audit gate present");
let end = rendered[start..]
.find("**Verify or feedback**")
.map_or(rendered.len(), |p| start + p);
let section = &rendered[start..end];
let lower = section.to_lowercase();
let mut hits = 0;
for cat in [
"command injection",
"xss",
"sql injection",
"path traversal",
"unvalidated external input",
"secret leakage",
] {
if lower.contains(cat) {
hits += 1;
}
}
assert!(
hits >= 4,
"Security audit must enumerate at least 4 of 6 OWASP categories; only {hits} found in:\n{section}",
);
assert!(
section.contains("unwrap()") || section.contains("expect()"),
"Security audit must mention the unwrap()/expect() rule; got:\n{section}",
);
}
#[test]
fn supervisor_skill_governance_verification_substep_preserved() {
let rendered = render_supervisor();
let start = rendered
.find("Governance verification")
.expect("Governance verification sub-step still present");
let end = (start + 2000).min(rendered.len());
let section = &rendered[start..end];
for needle in [
"DoD",
"ADR",
"security.md",
"test-strategy.md",
"constitution.md",
] {
assert!(
section.contains(needle),
"governance sub-step must still reference '{needle}'; got:\n{section}",
);
}
}
#[test]
fn coordination_skill_documents_commit_cadence() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("commit cadence") || lowered.contains("per-group commit cadence"),
"coordination skill should have a heading naming the commit-cadence concept; \
got:\n{}",
tmpl.content
);
assert!(
lowered.contains("group") || lowered.contains("section"),
"commit-cadence section should mention the GROUP/section grain"
);
let has_conventional_prefix = ["feat(", "fix(", "docs(", "test(", "chore("]
.iter()
.any(|p| tmpl.content.contains(p));
assert!(
has_conventional_prefix,
"commit-cadence section should show at least one conventional-commit prefix example"
);
}
#[test]
fn coordination_skill_forbids_opsx_verify_and_archive() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("/opsx:verify"),
"coordination skill should name `/opsx:verify` literally"
);
assert!(
tmpl.content.contains("/opsx:archive"),
"coordination skill should name `/opsx:archive` literally"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("off-limits")
|| lowered.contains("do not invoke")
|| lowered.contains("shall not")
|| lowered.contains("supervisor's job"),
"coordination skill should state both are not the coding agent's responsibility"
);
}
#[test]
fn coordination_skill_names_terminal_action() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content.contains("agent.artifact"),
"coordination skill should name `agent.artifact` as the terminal publish"
);
assert!(
tmpl.content.contains("\"done\"") || tmpl.content.contains("\"committed\""),
"coordination skill should reference status: \"done\" or \"committed\""
);
}
#[test]
fn supervisor_skill_documents_pane_current_path_resolution() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("tmux display-message"),
"supervisor skill should show the tmux display-message command"
);
assert!(
tmpl.content.contains("pane_current_path"),
"supervisor skill should name pane_current_path literally"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("not alphabetical")
|| lowered.contains("not sorted alphabetically")
|| lowered.contains("are not alphabetical"),
"supervisor skill should warn against alphabetical pane-index assumptions"
);
assert!(
lowered.contains("cli-argument order")
|| lowered.contains("cli argument order")
|| lowered.contains("argument order"),
"supervisor skill should warn against CLI-argument-order pane-index assumptions"
);
}
#[test]
fn supervisor_skill_documents_proactive_launch_sweep() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("launch-time pane sweep")
.or_else(|| lowered.find("launch sweep"))
.expect("launch-time pane sweep heading should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("immediately after attaching")
|| window.contains("before the poll thread")
|| window.contains("first-few-seconds")
|| window.contains("first few seconds"),
"launch sweep should link the sweep to the first-few-seconds-after-attach window",
);
}
#[test]
fn supervisor_skill_launch_sweep_escalates_unknown_via_agent_question() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("launch-time pane sweep")
.or_else(|| lowered.find("launch sweep"))
.expect("launch-time pane sweep heading should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("unknown") || window.contains("wider scope"),
"launch sweep should classify a third category for unknown/wider-scope prompts",
);
assert!(
window.contains("agent.question"),
"launch sweep should instruct agent.question escalation for unknown prompts",
);
assert!(
window.contains("escalate"),
"launch sweep should use the word 'escalate' alongside the agent.question instruction",
);
}
#[test]
fn supervisor_skill_launch_sweep_complements_auto_approve_thread() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("launch-time pane sweep")
.or_else(|| lowered.find("launch sweep"))
.expect("launch-time pane sweep heading should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("complements"),
"launch sweep should describe itself as complementing the auto-approve thread",
);
assert!(
window.contains("does not replace")
|| window.contains("not replace")
|| window.contains("does **not** replace"),
"launch sweep should explicitly say it does NOT replace the auto-approve thread",
);
assert!(
window.contains("[supervisor.auto_approve]") || window.contains("auto_approve"),
"launch sweep should cross-reference the [supervisor.auto_approve] poll thread",
);
}
#[test]
fn supervisor_skill_paste_buffer_cross_ref_in_send_keys_section() {
let tmpl = resolve("supervisor").unwrap();
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("send the answer to the agent pane")
.or_else(|| lowered.find("agents do not poll their inbox"))
.expect("send-keys-alongside-agent.feedback section should be present");
let window_end = (start + 2200).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("paste-buffer")
|| window.contains("paste buffer")
|| window.contains("follow-up enter")
|| window.contains("follow-up `enter`"),
"send-keys-alongside-feedback section must cross-reference paste-buffer recovery / follow-up Enter for long answers",
);
}
#[test]
fn supervisor_skill_warns_against_git_paw_status_ordering() {
let tmpl = resolve("supervisor").unwrap();
assert!(
tmpl.content.contains("git paw status"),
"supervisor skill should reference `git paw status` by name when warning against using its ordering as a mapping source",
);
let lowered = tmpl.content.to_lowercase();
let start = lowered
.find("pane_current_path")
.expect("pane_current_path resolution section should be present");
let window_end = (start + 2500).min(lowered.len());
let window = &lowered[start..window_end];
assert!(
window.contains("git paw status"),
"the warning against `git paw status` ordering must appear within the pane_current_path resolution section",
);
assert!(
window.contains("shall not be inferred")
|| window.contains("must not")
|| window.contains("not be inferred")
|| window.contains("not used as a mapping")
|| window.contains("no relationship"),
"section must forbid using `git paw status` order as a mapping source",
);
}
#[test]
fn coordination_skill_contains_context_budget_after_while_editing() {
let tmpl = resolve("coordination").unwrap();
let editing = tmpl
.content
.find("While you're editing")
.expect("coordination skill should contain 'While you're editing' heading");
let budget = tmpl
.content
.find("### Context budget")
.expect("coordination skill should contain a 'Context budget' heading");
assert!(
budget > editing,
"the 'Context budget' section must appear after the 'While you're editing' section"
);
}
#[test]
fn coordination_skill_context_budget_covers_three_topics() {
let tmpl = resolve("coordination").unwrap();
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("residual-budget heuristic"),
"context-budget section should cover the residual-budget heuristic"
);
assert!(
lowered.contains("when to compact, clear, or summarise"),
"context-budget section should cover the named compact/clear/summarise moments"
);
assert!(
lowered.contains("commit before you compact"),
"context-budget section should cover the commit-before-compact discipline"
);
}
#[test]
fn coordination_skill_residual_budget_heuristic_in_prose() {
let tmpl = resolve("coordination").unwrap();
let start = tmpl
.content
.find("### Context budget")
.expect("context-budget section present");
let end = tmpl.content[start..]
.find("### Check for messages")
.map_or(tmpl.content.len(), |o| start + o);
let section = &tmpl.content[start..end];
let lowered = section.to_lowercase();
assert!(
lowered.contains("60%") && lowered.contains("free"),
"residual-budget heuristic should reference keeping ~60% of the window free"
);
assert!(
lowered.contains("heuristic"),
"residual-budget guidance should be framed as a heuristic"
);
assert!(
lowered.contains("no config field")
|| lowered.contains("there is no\nconfig field")
|| lowered.contains("there is no config field"),
"the section should state there is no config field for the ratio"
);
}
#[test]
fn coordination_skill_three_moments_in_priority_order() {
let tmpl = resolve("coordination").unwrap();
let content = &tmpl.content;
let scenario = content
.find("After each spec scenario completes")
.expect("first moment present");
let working_set = content
.find("working set grows past")
.expect("second moment present");
let switching = content
.find("switching between sub-tasks")
.expect("third moment present");
assert!(
scenario < working_set && working_set < switching,
"the three named moments must appear in the documented priority order"
);
let first = &content[scenario..working_set];
let second = &content[working_set..switching];
let third = &content[switching..(switching + 300).min(content.len())];
assert!(
first.to_lowercase().contains("compact"),
"moment 1 should be labelled with the compact action"
);
assert!(
second.to_lowercase().contains("compact"),
"moment 2 should be labelled with the compact action"
);
assert!(
third.to_lowercase().contains("clear"),
"moment 3 should be labelled with the clear action"
);
}
#[test]
fn coordination_skill_states_commit_before_compact_discipline() {
let tmpl = resolve("coordination").unwrap();
assert!(
tmpl.content
.contains("**Never compact, clear, or summarise without first committing"),
"commit-before-compact discipline should be a bold, explicit statement"
);
let lowered = tmpl.content.to_lowercase();
assert!(
lowered.contains("agent.artifact"),
"the discipline should mention publishing an agent.artifact as the alternative to committing"
);
assert!(
lowered.contains("can't recover") || lowered.contains("cannot recover"),
"the discipline should pair the rule with a safety rationale about recoverability"
);
}
#[test]
fn coordination_skill_per_cli_mechanism_table() {
let tmpl = resolve("coordination").unwrap();
let start = tmpl
.content
.find("#### Per-CLI mechanism")
.expect("per-CLI mechanism subsection present");
let section = &tmpl.content[start..];
assert!(
section.contains("| `claude` | `/compact` | `/clear` |"),
"table should contain a claude row naming /compact and /clear"
);
assert!(
section.contains("| `claude-oss` | `/compact` | `/clear` |"),
"table should contain a claude-oss row naming /compact and /clear"
);
let other = section
.find("| other |")
.map(|o| §ion[o..(o + 200).min(section.len())])
.expect("table should contain an 'other' fallback row");
assert!(
other.contains("/compact") && other.contains("/save") && other.contains("/reset"),
"the 'other' row should point users at the CLI's /compact, /save, or /reset equivalent"
);
}
use crate::specs::SpecBackendKind;
fn render_skill(name: &str, backends: &[SpecBackendKind]) -> String {
let tmpl = resolve(name).unwrap_or_else(|_| panic!("resolve {name}"));
render(
&tmpl,
if name == "supervisor" {
"supervisor"
} else {
"feat/x"
},
"http://127.0.0.1:9119",
"git-paw",
&GateCommands::default(),
backends,
)
}
#[test]
fn coordination_lists_forbidden_commands_under_openspec() {
let out = render_skill("coordination", &[SpecBackendKind::OpenSpec]);
assert!(
out.contains("Commands you must not run"),
"coordination must carry the forbidden-command section"
);
assert!(out.contains("/opsx:verify"), "lists /opsx:verify");
assert!(out.contains("/opsx:archive"), "lists /opsx:archive");
assert!(
out.contains("supervisor-only"),
"names the commands supervisor-only"
);
assert!(
out.contains("role-gating guard"),
"references the role-gating guard"
);
}
#[test]
fn supervisor_has_must_must_not_section_under_openspec() {
let out = render_skill("supervisor", &[SpecBackendKind::OpenSpec]);
assert!(
out.contains("Commands you must run (not coding agents)"),
"supervisor must carry the supervisor-only section"
);
assert!(out.contains("/opsx:verify") && out.contains("/opsx:archive"));
assert!(out.contains("MUST") && out.contains("MUST NOT"));
let idx = out
.find("Commands you must run (not coding agents)")
.expect("section present");
let section = &out[idx..];
assert!(
section.contains("agent.feedback"),
"section instructs calling out violations via agent.feedback"
);
}
#[test]
fn supervisor_has_revert_flow_under_openspec() {
let out = render_skill("supervisor", &[SpecBackendKind::OpenSpec]);
assert!(
out.contains("Handling an opsx-role-gating revert request"),
"merge-orchestration carries the revert-request flow"
);
assert!(out.contains("git revert"), "teaches git revert");
assert!(
out.contains("auto_revert"),
"references the [supervisor] auto_revert opt-out"
);
}
#[test]
fn opsx_sections_omitted_under_non_openspec_engines() {
for backends in [
vec![SpecBackendKind::Markdown],
vec![SpecBackendKind::SpecKit],
vec![],
] {
let coord = render_skill("coordination", &backends);
assert!(
!coord.contains("Commands you must not run"),
"coordination forbidden section must be omitted for {backends:?}"
);
let sup = render_skill("supervisor", &backends);
assert!(
!sup.contains("Commands you must run (not coding agents)"),
"supervisor-only section must be omitted for {backends:?}"
);
assert!(
!sup.contains("Handling an opsx-role-gating revert request"),
"revert flow must be omitted for {backends:?}"
);
}
}
#[test]
fn opsx_region_markers_never_survive_rendering() {
for name in ["coordination", "supervisor"] {
for backends in [
vec![SpecBackendKind::OpenSpec],
vec![SpecBackendKind::Markdown],
vec![],
] {
let out = render_skill(name, &backends);
assert!(
!out.contains(OPSX_REGION_BEGIN) && !out.contains(OPSX_REGION_END),
"{name} under {backends:?} must not leak region markers"
);
}
}
}
#[test]
fn opsx_multi_backend_session_keeps_sections_when_openspec_present() {
let out = render_skill(
"supervisor",
&[SpecBackendKind::Markdown, SpecBackendKind::OpenSpec],
);
assert!(out.contains("Commands you must run (not coding agents)"));
}
#[test]
fn render_opsx_regions_strips_body_when_not_kept() {
let input = "before\n<!-- opsx-role-gating:begin -->\nSECRET\n<!-- opsx-role-gating:end -->\nafter\n";
let kept = render_opsx_regions(input, true);
assert!(kept.contains("SECRET"));
assert!(!kept.contains("opsx-role-gating:begin"));
let stripped = render_opsx_regions(input, false);
assert!(!stripped.contains("SECRET"));
assert!(stripped.contains("before") && stripped.contains("after"));
}
#[test]
fn raw_coordination_template_carries_the_forbidden_section() {
let tmpl = resolve("coordination").unwrap();
assert!(tmpl.content.contains("Commands you must not run"));
assert!(tmpl.content.contains(OPSX_REGION_BEGIN));
}
}