feat(TASK-038): add hooks builtin-asset category with example scripts and roles mixin scanning
- AssetCategory::Hooks + InstallFilter::Hooks (NAMES/parse/clap), reserving
the 'hooks' bundle name via the existing AssetCategory::parse check
- HookAssets embed + install_builtin_hooks(force): installs the bundled
example scripts 0755 into paths::hooks_dir() via write_file_atomic, wired
into install_builtins() and install_assets(); warns once when
COYOTE_HOOKS_DIR diverges from the config-dir default
- assets/hooks/notify.sh: notify-send -> osascript -> terminal fallback,
with AppleScript-literal sanitization of model-influenced values
- assets/hooks/log-events.sh: appends a COYOTE_* env snapshot (secrets
excluded, 0600 log, symlink-refusing) to ${COYOTE_HOOK_LOG:-/tmp/coyote-hooks.log}
- sbx-mixin discovery now scans roles/*.sbx-mixin.yaml and
roles/hooks/*.sbx-mixin.yaml (mirroring the role-hook cwd)
- tests: install+0755+force, install_assets(Hooks) end-to-end, discover()
roles scanning, reserved bundle name, no-ARGC-BUILD embed content, script
content order + real execution (secret exclusion, tty-less fallback),
CLI parser round-trips; hooks_dir env test made #[serial]
This commit is contained in:
9 files changed
+439
-4
No files matched your search
@@ -200,6 +200,19 @@ pub fn discover() -> Result<Vec<DiscoveredMixin>> {
|
||||
for path in collect_mixins(&paths::rags_dir(), &[ScanMode::Flat]) {
|
||||
out.push(read_mixin(path)?);
|
||||
}
|
||||
for path in collect_mixins(&paths::roles_dir(), &[ScanMode::Flat]) {
|
||||
out.push(read_mixin(path)?);
|
||||
}
|
||||
// Mirrors the role-hook working directory (role hook commands run with
|
||||
// cwd = roles_dir), so `./hooks/*.sbx-mixin.yaml` beside a role resolves
|
||||
// the same way the role's hook commands do — intentionally NOT
|
||||
// paths::hooks_dir(). The agent-hook working directory
|
||||
// (`<agents>/<agent>/hooks/` — agent hooks run with cwd = the agent's
|
||||
// data dir) is deliberately not scanned here either; agent-dir scanning
|
||||
// above covers `<agents>/<agent>/*.sbx-mixin.yaml` only.
|
||||
for path in collect_mixins(&paths::roles_dir().join("hooks"), &[ScanMode::Flat]) {
|
||||
out.push(read_mixin(path)?);
|
||||
}
|
||||
|
||||
if let Ok(cwd) = env::current_dir()
|
||||
&& let Some(path) = paths::find_workspace_sbx_mixin(&cwd)
|
||||
@@ -940,6 +953,55 @@ network:
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn discover_scans_roles_and_role_hooks_mixins() {
|
||||
use crate::utils::get_env_name;
|
||||
|
||||
let root = unique_root("discover-roles");
|
||||
let roles = root.join("roles");
|
||||
fs::create_dir_all(roles.join("hooks")).unwrap();
|
||||
let role_mixin = roles.join("review.sbx-mixin.yaml");
|
||||
let hook_mixin = roles.join("hooks").join("notify.sbx-mixin.yaml");
|
||||
fs::write(&role_mixin, "kind: mixin\n").unwrap();
|
||||
fs::write(&hook_mixin, "kind: mixin\n").unwrap();
|
||||
|
||||
let config_env = get_env_name("config_dir");
|
||||
let roles_env = get_env_name("roles_dir");
|
||||
let prev_config = env::var_os(&config_env);
|
||||
let prev_roles = env::var_os(&roles_env);
|
||||
unsafe {
|
||||
env::set_var(&config_env, &root);
|
||||
env::set_var(&roles_env, &roles);
|
||||
}
|
||||
|
||||
let discovered = discover();
|
||||
|
||||
unsafe {
|
||||
match prev_config {
|
||||
Some(v) => env::set_var(&config_env, v),
|
||||
None => env::remove_var(&config_env),
|
||||
}
|
||||
match prev_roles {
|
||||
Some(v) => env::set_var(&roles_env, v),
|
||||
None => env::remove_var(&roles_env),
|
||||
}
|
||||
}
|
||||
|
||||
let found: Vec<PathBuf> = discovered
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|mixin| mixin.path)
|
||||
.collect();
|
||||
assert!(found.contains(&role_mixin), "missing role mixin: {found:?}");
|
||||
assert!(
|
||||
found.contains(&hook_mixin),
|
||||
"missing role hook mixin: {found:?}"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// `generate_rag_sbx_mixin` writes an agent-scoped RAG sidecar next to the
|
||||
/// rag yaml, at `<agents>/<agent>/<rag>.sbx-mixin.yaml`. Before `SubdirFlat`
|
||||
/// existed, nothing scanned that shape and attaching a Qdrant RAG from
|
||||
|
||||
Reference in new issue
Block a user