Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/process-issues/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
name: process-issues
description: Resolve all open GitHub issues. Each issue becomes exactly one shipped PR. Trigger when user says "process issues", "work through issues", "resolve issues", "handle open issues", "fix all issues", or asks to resolve GitHub issues end-to-end.
user_invocable: true
metadata:
internal: true
---

Resolve all qualifying open GitHub issues. Each issue becomes exactly one merged PR. Do not stop until every issue is resolved or explicitly deferred.
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/ship/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
name: ship
description: Run the full ship flow — verify quality, ensure test coverage, update artifacts, smoke test, push, create PR, and merge when CI is green. Trigger when user says "ship", "ship it", "fix and ship", or asks to push and merge a branch.
user_invocable: true
metadata:
internal: true
---

Run the full ship flow: verify quality, ensure test coverage, update artifacts, smoke test, then push, create PR, and merge when CI is green.
Expand Down
157 changes: 157 additions & 0 deletions crates/bashkit/tests/agent_skills_publication_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Decision: assert the committed public skill inventory so private agent
// workflow skills stay hidden from `npx skills add everruns/bashkit`.

use std::fs;
use std::path::{Path, PathBuf};

#[test]
fn public_skill_inventory_is_only_bashkit() {
let repo = repo_root();
let mut public_skills = find_skill_mds(&repo)
.into_iter()
.filter_map(|path| {
let skill = parse_skill_md(&path);
(!skill.internal).then(|| (repo_relative(&repo, &path), skill.name))
})
.collect::<Vec<_>>();

public_skills.sort();

assert_eq!(
public_skills,
vec![("skills/bashkit/SKILL.md".to_string(), "bashkit".to_string())],
"only the public bashkit skill should be visible to default skill discovery"
);
}

#[test]
fn workflow_agent_skills_are_internal() {
let repo = repo_root();
let mut workflow_skills = find_skill_mds(&repo)
.into_iter()
.filter(|path| repo_relative(&repo, path).starts_with(".agents/skills/"))
.map(|path| {
let rel = repo_relative(&repo, &path);
let skill = parse_skill_md(&path);
(rel, skill.internal)
})
.collect::<Vec<_>>();

workflow_skills.sort();

assert!(
!workflow_skills.is_empty(),
"expected committed workflow skills under .agents/skills/"
);

let public = workflow_skills
.iter()
.filter_map(|(rel, internal)| (!internal).then_some(rel.as_str()))
.collect::<Vec<_>>();

assert!(
public.is_empty(),
"workflow skills must set metadata.internal: true: {public:?}"
);
}

#[derive(Debug)]
struct SkillFrontmatter {
name: String,
internal: bool,
}

fn parse_skill_md(path: &Path) -> SkillFrontmatter {
let text = fs::read_to_string(path).unwrap_or_else(|err| {
panic!("failed to read {}: {err}", path.display());
});

let mut lines = text.lines();
assert_eq!(
lines.next(),
Some("---"),
"{} must start with YAML frontmatter",
path.display()
);

let mut name = None;
let mut in_metadata = false;
let mut internal = false;

for line in lines {
if line == "---" {
break;
}

if let Some(value) = line.strip_prefix("name:") {
name = Some(value.trim().trim_matches('"').to_string());
in_metadata = false;
continue;
}

if line == "metadata:" {
in_metadata = true;
continue;
}

if !line.starts_with(' ') && !line.is_empty() {
in_metadata = false;
}

if in_metadata && line.trim() == "internal: true" {
internal = true;
}
}

SkillFrontmatter {
name: name.unwrap_or_else(|| panic!("{} is missing name frontmatter", path.display())),
internal,
}
}

fn find_skill_mds(root: &Path) -> Vec<PathBuf> {
let mut found = Vec::new();
walk(root, &mut found);
found
}

fn walk(dir: &Path, found: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).unwrap_or_else(|err| {
panic!("failed to read dir {}: {err}", dir.display());
}) {
let entry = entry.unwrap_or_else(|err| panic!("failed to read dir entry: {err}"));
let path = entry.path();
let file_name = entry.file_name();

if entry
.file_type()
.unwrap_or_else(|err| panic!("failed to stat {}: {err}", path.display()))
.is_dir()
{
if matches!(
file_name.to_str(),
Some(".git" | "target" | "node_modules" | ".venv" | "dist")
) {
continue;
}
walk(&path, found);
} else if file_name == "SKILL.md" {
found.push(path);
}
}
}

fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/bashkit should be two levels below repo root")
.to_path_buf()
}

fn repo_relative(repo: &Path, path: &Path) -> String {
path.strip_prefix(repo)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
Loading