mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 13:46:32 +03:00
Add DeepSeek Harness as a supported skills provider (#746)
* Add DeepSeek Harness as a supported skills provider npx impeccable install now detects ~/.dsh (or $DSH_HOME when it sits under home) and installs into ~/.dsh/skills, the user-level skill root DeepSeek Harness scans, with project-level .dsh/skills on the same layout as other providers. Aliases: dsh, deepseek, deepseek-harness. Engine: PROVIDER_DIRS / aliases / display / input order / global hint, $DSH_HOME-aware user skills dir, provider id resolution from the skill dir, pin harness dirs, bundle path normalization for hashing. Build: dsh transformer target emitting the frontmatter DeepSeek Harness reads (user-invocable, license, compatibility, metadata; unknown keys are ignored there) with no emitHooks (DSH hooks are in-process plugins, not on-disk manifests) and no agentFormat (no documented on-disk subagent format); placeholders (AGENTS.md config file, ask_user_question tool, / command prefix), provider block tags, universal README entry. Docs: HARNESSES.md row and frontmatter column, CLI-CONTRACT constants, README/DEVELOP/AGENTS provider lists. Validation: cargo test --workspace; node scripts/run-tests.mjs core (138 pass); bun run build (19 providers, dist/dsh artifact verified); engine smoke against a fake HOME with a local bundle: install --providers=dsh --scope=global, auto-detected install, and update all resolve the .dsh provider. Generated provider output intentionally omitted per repo policy; the sync workflow regenerates tracked .dsh/skills after merge. Prepared with AI assistance (DeepSeek Harness coding agent). * Address review: DSH_HOME-only detection, generated-output pathspecs - Detect DeepSeek Harness through the resolved $DSH_HOME (fallback ~/.dsh) instead of gating on a fixed ~/.dsh path, so a DSH_HOME-only setup is offered by a provider-less install; generalize the two env-relocated config-dir hints (OpenCode, DSH) into one shared probe. - Add .dsh to the sync workflow's GENERATED_PATHS and CI's generated drift check so the tracked .dsh/skills payload is committed and validated. - Cover both behaviors: new install_detection_tests (DSH_HOME-only, default ~/.dsh, refused outside-home override) and a CLI-CONTRACT note on the resolved detection path. Validation: cargo test --workspace; node scripts/run-tests.mjs core (138 pass); engine smoke: DSH_HOME-only fake HOME installs globally into the resolved skills dir. Prepared with AI assistance (DeepSeek Harness coding agent). * Fix DeepSeek Harness home paths on Windows Use native relative-path containment, cover case and drive boundaries, and verify relocated global install/update without changing project skills. Add DSH output coverage and correct the install documentation. AI assistance: Codex, under pbakaus maintainer direction. * Document the CLI limit on external DSH homes Clarify that outside-home manual copies are not detected or updated by the CLI. AI assistance: Codex, under pbakaus maintainer direction. --------- Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
This commit is contained in:
co-authored by
Paul Bakaus
parent
f41300b3b2
commit
6ebc24ad66
@@ -137,7 +137,7 @@ jobs:
|
||||
# What `bun run build` writes, which runs on every PR.
|
||||
# extension/detector/ is gitignored (built by `cargo xtask bundle`);
|
||||
# it stays listed so a stray tracked copy shows up here.
|
||||
run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin extension/detector
|
||||
run: git diff --exit-code -- .agents .claude .cursor .dsh .gemini .github/skills plugin extension/detector
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -26,6 +26,7 @@ env:
|
||||
.codex
|
||||
.claude
|
||||
.cursor
|
||||
.dsh
|
||||
.gemini
|
||||
.github/agents
|
||||
.github/hooks
|
||||
|
||||
@@ -24,7 +24,7 @@ Run `bun run build` after changing anything in `skill/`, transformer code, or us
|
||||
|
||||
## Generated Provider Output Policy
|
||||
|
||||
The root harness folders (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `.gemini/skills/`, `.github/skills/`, `.grok/skills/`, `.hermes/skills/`, `.kiro/skills/`, `.opencode/skills/`, `.pi/skills/`, `.qoder/skills/`, `.rovodev/skills/`, `.trae*/skills/`, `.vibe/skills/`) and `plugin/` stay tracked so `main` remains installable for direct GitHub, `npx skills`, and submodule users. They are still generated artifacts.
|
||||
The root harness folders (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, `.dsh/skills/`, `.gemini/skills/`, `.github/skills/`, `.grok/skills/`, `.hermes/skills/`, `.kiro/skills/`, `.opencode/skills/`, `.pi/skills/`, `.qoder/skills/`, `.rovodev/skills/`, `.trae*/skills/`, `.vibe/skills/`) and `plugin/` stay tracked so `main` remains installable for direct GitHub, `npx skills`, and submodule users. They are still generated artifacts.
|
||||
|
||||
Normal development should be source-first: stage changes in `crates/`, `browser-bundle/`, `skill/`, `scripts/`, `cli/`, `extension/`, and `tests/`; leave generated harness churn unstaged unless the user asked for it. After source changes land on `main`, `.github/workflows/sync-generated-output.yml` runs `bun run build:release` and commits generated provider output directly back to `main`. Treat generated harness diffs as release artifacts and keep them out of feature PRs unless they are the point of the PR. The two tracked engine assets under `crates/live/assets/` follow the rule-change workflow below instead.
|
||||
|
||||
|
||||
@@ -184,6 +184,18 @@ cp -r dist/claude-code/.claude/* ~/.claude/
|
||||
cp -r dist/opencode/.opencode your-project/
|
||||
```
|
||||
|
||||
**DeepSeek Harness:**
|
||||
```bash
|
||||
# Project-specific
|
||||
cp -r dist/dsh/.dsh your-project/
|
||||
|
||||
# Or global (applies to all projects)
|
||||
mkdir -p "${DSH_HOME:-$HOME/.dsh}/skills"
|
||||
cp -r dist/dsh/.dsh/skills/* "${DSH_HOME:-$HOME/.dsh}/skills/"
|
||||
```
|
||||
|
||||
The CLI honors `DSH_HOME` only when it resolves inside your home directory (or to home itself); otherwise it uses `~/.dsh`. An outside-home manual copy is not managed by `impeccable install/update`.
|
||||
|
||||
**Hermes Agent:**
|
||||
```bash
|
||||
# Global (applies to all projects; uses the active profile, or ~/.hermes by default)
|
||||
@@ -444,6 +456,7 @@ Full detector docs: [impeccable.style/docs/detector](https://impeccable.style/do
|
||||
- [Cursor](https://cursor.com)
|
||||
- [Claude Code](https://claude.ai/code)
|
||||
- [GitHub Copilot](https://github.com/features/copilot)
|
||||
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
||||
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
|
||||
- [Codex CLI](https://github.com/openai/codex)
|
||||
- [Grok Build](https://x.ai/cli)
|
||||
|
||||
@@ -8,8 +8,8 @@ use impeccable_common::Io;
|
||||
/// keep in sync with the public repo).
|
||||
pub const COMMAND_METADATA_JSON: &str = include_str!("command-metadata.json");
|
||||
|
||||
const HARNESS_DIRS: [&str; 17] = [
|
||||
".claude", ".cursor", ".gemini", ".codex", ".agents", ".agent", ".github", ".grok", ".hermes", ".trae", ".trae-cn",
|
||||
const HARNESS_DIRS: [&str; 18] = [
|
||||
".claude", ".cursor", ".dsh", ".gemini", ".codex", ".agents", ".agent", ".github", ".grok", ".hermes", ".trae", ".trae-cn",
|
||||
".pi", ".opencode", ".kiro", ".rovodev", ".vibe", ".qoder",
|
||||
];
|
||||
const CODEX_HARNESSES: [&str; 2] = [".codex", ".agents"];
|
||||
|
||||
@@ -62,6 +62,7 @@ fn provider_from_skill_dir(skill_dir: &str) -> Option<&'static str> {
|
||||
Some(match harness.as_str() {
|
||||
".claude" => "claude-code",
|
||||
".cursor" => "cursor",
|
||||
".dsh" => "dsh",
|
||||
".gemini" => "gemini",
|
||||
".codex" => "codex",
|
||||
".agents" => "agents",
|
||||
|
||||
@@ -405,7 +405,7 @@ pub fn list_skill_tree_files(root: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
static PROVIDER_PATH_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"\.(claude|cursor|agents|agent|github|gemini|codex|grok|hermes|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe)/skills/").unwrap()
|
||||
Regex::new(r"\.(claude|cursor|dsh|agents|agent|github|gemini|codex|grok|hermes|kiro|opencode|pi|qoder|trae|trae-cn|rovodev|vibe)/skills/").unwrap()
|
||||
});
|
||||
|
||||
/// JS: normalizeForHash(content)
|
||||
|
||||
+107
-10
@@ -8,7 +8,7 @@ use crate::util::{self, jsp, Env};
|
||||
pub const API_BASE: &str = "https://impeccable.style";
|
||||
|
||||
pub const PROVIDER_DIRS: &[&str] = &[
|
||||
".claude", ".cursor", ".gemini", ".agents", ".agent", ".github", ".grok", ".hermes", ".kiro",
|
||||
".claude", ".cursor", ".dsh", ".gemini", ".agents", ".agent", ".github", ".grok", ".hermes", ".kiro",
|
||||
".opencode", ".pi", ".qoder", ".trae", ".trae-cn", ".rovodev", ".vibe",
|
||||
];
|
||||
|
||||
@@ -21,6 +21,9 @@ const PROVIDER_ALIASES: &[(&str, &str)] = &[
|
||||
("codex", ".agents"),
|
||||
("copilot", ".github"),
|
||||
("cursor", ".cursor"),
|
||||
("deepseek", ".dsh"),
|
||||
("deepseek-harness", ".dsh"),
|
||||
("dsh", ".dsh"),
|
||||
("gemini", ".gemini"),
|
||||
("github", ".github"),
|
||||
("grok", ".grok"),
|
||||
@@ -43,6 +46,7 @@ const PROVIDER_DISPLAY: &[(&str, &str, &str)] = &[
|
||||
(".agents", "Codex CLI", "codex"),
|
||||
(".claude", "Claude Code", "claude"),
|
||||
(".cursor", "Cursor", "cursor"),
|
||||
(".dsh", "DeepSeek Harness", "dsh"),
|
||||
(".gemini", "Gemini CLI", "gemini"),
|
||||
(".github", "GitHub Copilot", "github"),
|
||||
(".grok", "Grok Build", "grok"),
|
||||
@@ -58,7 +62,7 @@ const PROVIDER_DISPLAY: &[(&str, &str, &str)] = &[
|
||||
];
|
||||
|
||||
pub const PROVIDER_INPUT_ORDER: &[&str] = &[
|
||||
"antigravity", "claude", "codex", "cursor", "gemini", "github", "grok", "hermes", "kiro",
|
||||
"antigravity", "claude", "codex", "cursor", "dsh", "gemini", "github", "grok", "hermes", "kiro",
|
||||
"opencode", "pi", "qoder", "trae", "trae-cn", "rovo-dev", "vibe",
|
||||
];
|
||||
|
||||
@@ -89,10 +93,30 @@ pub fn hermes_global_home(env: &Env, cwd: &str, home: &str) -> String {
|
||||
jsp::join(&[home, ".hermes"])
|
||||
}
|
||||
|
||||
/// JS: dshGlobalHome(home): honor $DSH_HOME only when it sits under `home`
|
||||
/// (resolved against cwd like `path.resolve`), mirroring hermesGlobalHome.
|
||||
pub fn dsh_global_home(env: &Env, cwd: &str, home: &str) -> String {
|
||||
if let Some(env_home) = env.get("DSH_HOME").filter(|v| !v.is_empty()) {
|
||||
let resolved_env = jsp::resolve(cwd, &[env_home]);
|
||||
let resolved_home = jsp::resolve(cwd, &[home]);
|
||||
// Native path.relative handles Windows separators, drive/UNC roots,
|
||||
// and case folding without accepting a sibling with the same prefix.
|
||||
let relative = jsp::relative(cwd, &resolved_home, &resolved_env);
|
||||
if !jsp::is_absolute(&relative)
|
||||
&& relative != ".."
|
||||
&& !relative.starts_with(&format!("..{}", jsp::SEP))
|
||||
{
|
||||
return resolved_env;
|
||||
}
|
||||
}
|
||||
jsp::join(&[home, ".dsh"])
|
||||
}
|
||||
|
||||
/// JS: HOME_SKILLS_DIR_OVERRIDES[provider]?.(home)
|
||||
fn home_skills_dir_override(env: &Env, cwd: &str, provider: &str, home: &str) -> Option<String> {
|
||||
match provider {
|
||||
".agent" => Some(jsp::join(&[home, ".gemini", "config", "skills"])),
|
||||
".dsh" => Some(jsp::join(&[&dsh_global_home(env, cwd, home), "skills"])),
|
||||
".hermes" => Some(jsp::join(&[&hermes_global_home(env, cwd, home), "skills"])),
|
||||
".pi" => Some(jsp::join(&[home, ".pi", "agent", "skills"])),
|
||||
".opencode" => Some(jsp::join(&[&opencode_global_config_dir(env, home), "skills"])),
|
||||
@@ -101,7 +125,7 @@ fn home_skills_dir_override(env: &Env, cwd: &str, provider: &str, home: &str) ->
|
||||
}
|
||||
|
||||
fn has_home_override(provider: &str) -> bool {
|
||||
matches!(provider, ".agent" | ".hermes" | ".pi" | ".opencode")
|
||||
matches!(provider, ".agent" | ".dsh" | ".hermes" | ".pi" | ".opencode")
|
||||
}
|
||||
|
||||
/// Everything the scans need from the process: env, cwd, and the resolved
|
||||
@@ -309,6 +333,16 @@ impl Sys {
|
||||
has_real_skills: has_real_skill_entries(&jsp::join(&[root, provider, "skills"])),
|
||||
});
|
||||
}
|
||||
// Shared probe pair for env-relocated config dirs (OpenCode, DSH):
|
||||
// the harness-specific user skills dir plus a direct
|
||||
// `<config-dir>/skills` fallback.
|
||||
let config_dir_detection = |provider: &'static str, found: String| {
|
||||
let probes = unique_paths(vec![
|
||||
self.user_provider_skills_dir(home, provider),
|
||||
jsp::join(&[&found, "skills"]),
|
||||
]);
|
||||
(found, probes)
|
||||
};
|
||||
for hint in GLOBAL_HARNESS_HINTS {
|
||||
let (found_path, probe_paths) = match hint {
|
||||
Hint::Home(rel, provider) => {
|
||||
@@ -320,12 +354,10 @@ impl Sys {
|
||||
(found, probes)
|
||||
}
|
||||
Hint::OpencodeConfig(provider) => {
|
||||
let found = opencode_global_config_dir(&self.env, home);
|
||||
let probes = unique_paths(vec![
|
||||
self.user_provider_skills_dir(home, provider),
|
||||
jsp::join(&[&found, "skills"]),
|
||||
]);
|
||||
(found, probes)
|
||||
config_dir_detection(provider, opencode_global_config_dir(&self.env, home))
|
||||
}
|
||||
Hint::DshHome(provider) => {
|
||||
config_dir_detection(provider, dsh_global_home(&self.env, &self.cwd, home))
|
||||
}
|
||||
};
|
||||
if !util::exists(&found_path) {
|
||||
@@ -415,12 +447,16 @@ pub enum UpdateTarget {
|
||||
enum Hint {
|
||||
Home(&'static str, &'static str),
|
||||
OpencodeConfig(&'static str),
|
||||
/// `$DSH_HOME` relocates the whole config root, so detection must probe
|
||||
/// the resolved home (which falls back to `~/.dsh`) rather than a fixed
|
||||
/// relative path; a plain `Home` hint would miss a DSH_HOME-only setup.
|
||||
DshHome(&'static str),
|
||||
}
|
||||
|
||||
impl Hint {
|
||||
fn provider(&self) -> &'static str {
|
||||
match self {
|
||||
Hint::Home(_, p) | Hint::OpencodeConfig(p) => p,
|
||||
Hint::Home(_, p) | Hint::OpencodeConfig(p) | Hint::DshHome(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,6 +469,7 @@ const GLOBAL_HARNESS_HINTS: &[Hint] = &[
|
||||
Hint::Home(".claude", ".claude"),
|
||||
Hint::Home(".codex", ".agents"),
|
||||
Hint::Home(".cursor", ".cursor"),
|
||||
Hint::DshHome(".dsh"),
|
||||
Hint::Home(".gemini", ".gemini"),
|
||||
Hint::Home(".grok", ".grok"),
|
||||
Hint::Home(".hermes", ".hermes"),
|
||||
@@ -758,6 +795,66 @@ mod tests {
|
||||
assert_eq!(invalid, vec!["zzz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsh_provider_resolves() {
|
||||
assert_eq!(normalize_provider_name("dsh"), Some(".dsh"));
|
||||
assert_eq!(normalize_provider_name("deepseek"), Some(".dsh"));
|
||||
assert_eq!(normalize_provider_name("deepseek-harness"), Some(".dsh"));
|
||||
assert_eq!(normalize_provider_name(".dsh"), Some(".dsh"));
|
||||
assert_eq!(provider_display_name(".dsh"), "DeepSeek Harness");
|
||||
assert_eq!(provider_input_name(".dsh"), "dsh");
|
||||
|
||||
// Default global skills dir is ~/.dsh/skills; $DSH_HOME wins only
|
||||
// when it sits under home, like $HERMES_HOME for .hermes.
|
||||
let cwd = if cfg!(windows) { r"C:\work" } else { "/work" };
|
||||
let home = if cfg!(windows) { r"C:\Users\u" } else { "/home/u" };
|
||||
let default = jsp::join(&[home, ".dsh"]);
|
||||
let custom = jsp::join(&[home, "custom-dsh"]);
|
||||
let env = Env::new();
|
||||
assert_eq!(dsh_global_home(&env, cwd, home), default);
|
||||
let mut env = Env::new();
|
||||
env.insert("DSH_HOME".into(), custom.clone());
|
||||
assert_eq!(dsh_global_home(&env, cwd, home), custom);
|
||||
env.insert("DSH_HOME".into(), "/elsewhere/dsh".into());
|
||||
assert_eq!(dsh_global_home(&env, cwd, home), default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsh_home_respects_resolved_path_boundaries() {
|
||||
let home = if cfg!(windows) { r"C:\Users\Test User" } else { "/home/Test User" };
|
||||
let cwd = jsp::join(&[home, "project"]);
|
||||
let default = jsp::join(&[home, ".dsh"]);
|
||||
for (value, expected) in [
|
||||
("../custom dsh".to_string(), jsp::join(&[home, "custom dsh"])),
|
||||
(home.to_string(), home.to_string()),
|
||||
(String::new(), default.clone()),
|
||||
(format!("{home}-other/dsh"), default.clone()),
|
||||
(format!("{home}/../outside"), default),
|
||||
] {
|
||||
let env = Env::from([("DSH_HOME".into(), value.clone())]);
|
||||
assert_eq!(dsh_global_home(&env, &cwd, home), expected, "DSH_HOME={value}");
|
||||
}
|
||||
// A drive/filesystem root has no extra separator to append.
|
||||
let root = if cfg!(windows) { "C:\\" } else { "/" };
|
||||
let child = jsp::join(&[root, "custom dsh"]);
|
||||
let env = Env::from([("DSH_HOME".into(), child.clone())]);
|
||||
assert_eq!(dsh_global_home(&env, root, root), child);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn dsh_home_handles_windows_case_separators_and_devices() {
|
||||
for (home, value, expected) in [
|
||||
(r"C:\Users\Alice", "c:/users/alice/custom", r"c:\users\alice\custom"),
|
||||
(r"C:\Users\Alice", r"D:\Users\Alice\custom", r"C:\Users\Alice\.dsh"),
|
||||
(r"\\server\share\Alice", r"\\server\share\Alice\custom", r"\\server\share\Alice\custom"),
|
||||
(r"\\server\share\Alice", r"\\server\other\Alice\custom", r"\\server\share\Alice\.dsh"),
|
||||
] {
|
||||
let env = Env::from([("DSH_HOME".into(), value.into())]);
|
||||
assert_eq!(dsh_global_home(&env, home, home), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_extraction() {
|
||||
// Values recorded from origin/main's parseSkillFrontmatterVersion (#703).
|
||||
|
||||
@@ -397,6 +397,40 @@ fn claude_install_and_update_backfill_bundled_agents() {
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsh_install_and_update_use_relocated_home_without_touching_project_skills() {
|
||||
let root = temp_root("dsh-install-update");
|
||||
let project = jsp::join(&[&root, "project"]);
|
||||
let home = jsp::join(&[&root, "home"]);
|
||||
let tmpdir = jsp::join(&[&root, "tmp"]);
|
||||
let dsh_home = jsp::join(&[&home, "custom dsh"]);
|
||||
for dir in [&project, &home, &tmpdir, &dsh_home] {
|
||||
std::fs::create_dir_all(dir).unwrap();
|
||||
}
|
||||
std::fs::create_dir_all(jsp::join(&[&project, ".git"])).unwrap();
|
||||
let bundle = create_fake_universal_bundle(&root, &[".dsh"]);
|
||||
let mut env = base_env(&home, &tmpdir, &bundle);
|
||||
env.insert("DSH_HOME".into(), dsh_home.clone());
|
||||
let project_skill = jsp::join(&[&project, ".dsh", "skills", "impeccable", "SKILL.md"]);
|
||||
write(&project_skill, "project skill must stay untouched");
|
||||
let installed = jsp::join(&[&dsh_home, "skills", "impeccable", "SKILL.md"]);
|
||||
let source = jsp::join(&[&bundle, ".dsh", "skills", "impeccable", "SKILL.md"]);
|
||||
|
||||
let r = run_cli(&["install", "-y", "--providers=deepseek-harness", "--scope=global"], &project, &env);
|
||||
assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr);
|
||||
assert_eq!(read(&installed), read(&source));
|
||||
assert!(!std::path::Path::new(&jsp::join(&[&home, ".dsh"])).exists());
|
||||
|
||||
write(&source, "---\nname: impeccable\nversion: 9.9.10-local\n---\n\nUpdated DSH fixture.\n");
|
||||
let r = run_cli(&["update", "-y", "--providers=dsh", "--scope=global"], &project, &env);
|
||||
assert_eq!(r.code, 0, "{}\n{}", r.stdout, r.stderr);
|
||||
assert_eq!(read(&installed), read(&source));
|
||||
assert_eq!(read(&project_skill), "project skill must stay untouched");
|
||||
assert!(!std::path::Path::new(&jsp::join(&[&dsh_home, "agents"])).exists());
|
||||
assert!(!std::path::Path::new(&jsp::join(&[&dsh_home, "hooks"])).exists());
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
// ─── home-scoped agent freshness (16a218e6) ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Detection coverage for env-relocated global harness dirs: `$DSH_HOME`
|
||||
//! (DeepSeek Harness) must be detected even when `~/.dsh` itself does not
|
||||
//! exist, and must be ignored when it points outside home.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use impeccable_skills::providers::{Scope, Sys};
|
||||
use impeccable_common::jsp;
|
||||
|
||||
fn temp_root(name: &str) -> String {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let dir = std::env::temp_dir().join(format!("impeccable-{name}-{}-{nanos}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("temp root");
|
||||
let real = dir.canonicalize().unwrap().to_string_lossy().into_owned();
|
||||
real.strip_prefix(r"\\?\").map(str::to_string).unwrap_or(real)
|
||||
}
|
||||
|
||||
fn sys_with(home: &str, extra: &[(&str, &str)]) -> Sys {
|
||||
let mut env: HashMap<String, String> = HashMap::new();
|
||||
env.insert("HOME".into(), home.to_string());
|
||||
env.insert("USERPROFILE".into(), home.to_string());
|
||||
for (k, v) in extra {
|
||||
env.insert((*k).to_string(), (*v).to_string());
|
||||
}
|
||||
Sys::new(env, home.to_string())
|
||||
}
|
||||
|
||||
fn dsh_detections(sys: &Sys, root: &str) -> Vec<String> {
|
||||
sys.collect_install_detections(root)
|
||||
.into_iter()
|
||||
.filter(|d| d.provider == ".dsh" && d.scope == Scope::User)
|
||||
.map(|d| d.found_path)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsh_home_only_setup_is_detected() {
|
||||
let home = temp_root("dsh-home-detect");
|
||||
let project = temp_root("dsh-home-project");
|
||||
let dsh_home = jsp::join(&[&home, "custom-dsh"]);
|
||||
std::fs::create_dir_all(&dsh_home).unwrap();
|
||||
let sys = sys_with(&home, &[("DSH_HOME", &dsh_home)]);
|
||||
|
||||
let found = dsh_detections(&sys, &project);
|
||||
assert_eq!(found, vec![dsh_home.clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_dot_dsh_is_detected_without_env() {
|
||||
let home = temp_root("dsh-default-detect");
|
||||
let project = temp_root("dsh-default-project");
|
||||
std::fs::create_dir_all(jsp::join(&[&home, ".dsh"])).unwrap();
|
||||
let sys = sys_with(&home, &[]);
|
||||
|
||||
let found = dsh_detections(&sys, &project);
|
||||
assert_eq!(found, vec![jsp::join(&[&home, ".dsh"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dsh_home_outside_home_falls_back_to_dot_dsh() {
|
||||
let home = temp_root("dsh-outside-detect");
|
||||
let project = temp_root("dsh-outside-project");
|
||||
let outside = temp_root("dsh-outside-elsewhere");
|
||||
std::fs::create_dir_all(&outside).unwrap();
|
||||
let sys = sys_with(&home, &[("DSH_HOME", &outside)]);
|
||||
|
||||
// No ~/.dsh and the env override is refused: not detected.
|
||||
assert!(dsh_detections(&sys, &project).is_empty());
|
||||
|
||||
// With ~/.dsh present the fallback detects the default location.
|
||||
std::fs::create_dir_all(jsp::join(&[&home, ".dsh"])).unwrap();
|
||||
let found = dsh_detections(&sys, &project);
|
||||
assert_eq!(found, vec![jsp::join(&[&home, ".dsh"])]);
|
||||
}
|
||||
@@ -354,7 +354,7 @@ retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNI
|
||||
|
||||
- **Invoked from**: README.md ("npx impeccable install / update"), README.npm.md Quick Start (`npx impeccable skills install`, `... install -y --providers=claude,codex --scope=project`, `... update`, `... install --no-hooks`, `... link --source=.impeccable --providers=claude,cursor`, `... skills help`), `README.md:360` (hook consent explanation).
|
||||
- `run(args)`: `args[0]` ∈ `undefined|help|--help|-h` → `showHelp()`; `install` → `install(rest)`; `link`; `update`; `check` (ignores flags); else `stderr> Unknown skills command: ${sub}` + `Run 'impeccable --help' for available commands.`, `exit 1`.
|
||||
- Constants: `API_BASE = 'https://impeccable.style'`; `PROVIDER_DIRS = ['.claude','.cursor','.gemini','.agents','.agent','.github','.grok','.hermes','.kiro','.opencode','.pi','.qoder','.trae','.trae-cn','.rovodev','.vibe']`; aliases (`agent`→`.agent`, `agents`/`codex`→`.agents`, `antigravity`→`.agent`, `claude`/`claude-code`→`.claude`, `copilot`/`github`→`.github`, `cursor`, `gemini`, `grok`/`grok-build`/`xai`→`.grok`, `hermes`, `kiro`, `opencode`, `pi`, `qoder`, `rovo-dev`/`rovodev`→`.rovodev`, `trae`, `trae-cn`, `vibe`); leading `.` stripped and lowercased before alias lookup; a literal PROVIDER_DIR value is accepted as-is. `DEFAULT_TARGETS = ['.claude','.agents']`. User-scope skill dir overrides: `.agent`→`~/.gemini/config/skills`, `.hermes`→`$HERMES_HOME/skills` (only when HERMES_HOME under home) else `~/.hermes/skills`, `.pi`→`~/.pi/agent/skills`, `.opencode`→`$OPENCODE_CONFIG_DIR|$XDG_CONFIG_HOME/opencode|~/.config/opencode` + `/skills`; others `~/<provider>/skills`. Project scope: `<root>/<provider>/skills`.
|
||||
- Constants: `API_BASE = 'https://impeccable.style'`; `PROVIDER_DIRS = ['.claude','.cursor','.dsh','.gemini','.agents','.agent','.github','.grok','.hermes','.kiro','.opencode','.pi','.qoder','.trae','.trae-cn','.rovodev','.vibe']`; aliases (`agent`→`.agent`, `agents`/`codex`→`.agents`, `antigravity`→`.agent`, `claude`/`claude-code`→`.claude`, `copilot`/`github`→`.github`, `cursor`, `deepseek`/`deepseek-harness`/`dsh`→`.dsh`, `gemini`, `grok`/`grok-build`/`xai`→`.grok`, `hermes`, `kiro`, `opencode`, `pi`, `qoder`, `rovo-dev`/`rovodev`→`.rovodev`, `trae`, `trae-cn`, `vibe`); leading `.` stripped and lowercased before alias lookup; a literal PROVIDER_DIR value is accepted as-is. `DEFAULT_TARGETS = ['.claude','.agents']`. User-scope skill dir overrides: `.agent`→`~/.gemini/config/skills`, `.dsh`→`$DSH_HOME/skills` (only when DSH_HOME under home) else `~/.dsh/skills`, `.hermes`→`$HERMES_HOME/skills` (only when HERMES_HOME under home) else `~/.hermes/skills`, `.pi`→`~/.pi/agent/skills`, `.opencode`→`$OPENCODE_CONFIG_DIR|$XDG_CONFIG_HOME/opencode|~/.config/opencode` + `/skills`; others `~/<provider>/skills`. Project scope: `<root>/<provider>/skills`.
|
||||
- **help**: `fetch('https://impeccable.style/api/commands')` → JSON array `[{id, description}]`; failure → `stderr> Could not fetch command list from impeccable.style. Check your network connection.`, `exit 1`. Prints:
|
||||
```
|
||||
|
||||
@@ -375,7 +375,7 @@ retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNI
|
||||
```
|
||||
sorted by `id.localeCompare`.
|
||||
- **Flag parsing** (`getFlagValue`): `--name=value` or `--name value` (next arg not starting with `-`). Boolean flags via `includes`.
|
||||
- **install flags**: `--force`, `-y|--yes`, `--no-hooks`, `--providers=<list>`, scope: `--user|--home|--global` → user; `--project|--local` → project; `--scope=<v>`/`--install-scope=<v>` normalized (`u|user|home|global`→user, `p|project|local|repo`→project; unknown → error `Unknown install scope: ${v}. Use --scope=project or --scope=global.`). Project root = nearest ancestor with `.git`, else cwd. Detection: project harness dirs present in root, plus `GLOBAL_HARNESS_HINTS` under home (`.agent`, `.gemini/antigravity*`→`.agent`, `.claude`, `.codex`→`.agents`, `.cursor`, `.gemini`, `.grok`, `.hermes`, `.kiro`, `.opencode`, opencode config dir, `.pi`, `.qoder`, `.rovodev`, `.vibe`). Targets: explicit list wins (invalid names → `Unknown provider(s): ...`); `-y` → detected project providers, else detected user providers, else DEFAULT_TARGETS; interactive → prints "Detected harnesses:" table then radio/checkbox prompts (raw-mode TTY) or line prompts (`Install target: [1] Detected only (...) [2] Customize [1]: `, `Select harnesses (comma-separated: ...)`). Scope: explicit; `-y` → project; interactive prompt `Install location` (Project/Global). Hooks: `decideHookInstall` reads `hook.consent` in config(.local).json; declined→false, accepted→true; all targets already have hook markers→true; `-y` or non-TTY→true; else prints HOOK_EXPLAINER and asks `Install the design hook? (Y/n) `, storing consent in `.impeccable/config.local.json`.
|
||||
- **install flags**: `--force`, `-y|--yes`, `--no-hooks`, `--providers=<list>`, scope: `--user|--home|--global` → user; `--project|--local` → project; `--scope=<v>`/`--install-scope=<v>` normalized (`u|user|home|global`→user, `p|project|local|repo`→project; unknown → error `Unknown install scope: ${v}. Use --scope=project or --scope=global.`). Project root = nearest ancestor with `.git`, else cwd. Detection: project harness dirs present in root, plus `GLOBAL_HARNESS_HINTS` under home (`.agent`, `.gemini/antigravity*`→`.agent`, `.claude`, `.codex`→`.agents`, `.cursor`, `.dsh` (resolved `$DSH_HOME`, fallback `~/.dsh`), `.gemini`, `.grok`, `.hermes`, `.kiro`, `.opencode`, opencode config dir, `.pi`, `.qoder`, `.rovodev`, `.vibe`). Targets: explicit list wins (invalid names → `Unknown provider(s): ...`); `-y` → detected project providers, else detected user providers, else DEFAULT_TARGETS; interactive → prints "Detected harnesses:" table then radio/checkbox prompts (raw-mode TTY) or line prompts (`Install target: [1] Detected only (...) [2] Customize [1]: `, `Select harnesses (comma-separated: ...)`). Scope: explicit; `-y` → project; interactive prompt `Install location` (Project/Global). Hooks: `decideHookInstall` reads `hook.consent` in config(.local).json; declined→false, accepted→true; all targets already have hook markers→true; `-y` or non-TTY→true; else prints HOOK_EXPLAINER and asks `Install the design hook? (Y/n) `, storing consent in `.impeccable/config.local.json`.
|
||||
Bundle: `IMPECCABLE_BUNDLE_PATH` (dir or zip) else download `https://impeccable.style/api/download/bundle/universal` (https `get`, one redirect followed, non-200 → `HTTP ${status}`) to `${tmpdir}/impeccable-update-${Date.now()}.zip`, extracted with `fflate.unzipSync` into `${tmpdir}/impeccable-update-${Date.now()}` (zip-slip guarded: `Refusing to extract entry outside target dir: ${entry}`). Bundle layout `<bundle>/<provider>/skills/<skill>/...`, `<bundle>/<provider>/agents/*.md`, hook manifests `<bundle>/.claude/settings.json`, `.cursor/hooks.json`, `.codex/hooks.json`, `.github/hooks/impeccable.json`, `.grok/hooks/impeccable.json`.
|
||||
Fresh install: `stdout> \nDownloading impeccable skills...`; migrate `*-impeccable` prefixed dirs → `impeccable`; copy each skill dir (rm existing dest first; drop an in-project cross-provider symlinked skills dir); copy agents (`.github`→`.github/agents` or `~/.copilot/agents`; `.cursor`→`.cursor/agents` or `~/.cursor/agents`); write hooks (dest: `.claude/settings.local.json` [skips if `.claude/settings.json` already carries the marker and prunes the local copy], `.cursor/hooks.json`, `.codex/hooks.json` for `.agents`, `.github/hooks/impeccable.json`, `.grok/hooks/impeccable.json`); merged with existing JSON (`mergeHookManifests`: strips existing impeccable entries by markers `skills/impeccable/scripts/hook-probe.mjs|hook.mjs|hook-before-edit.mjs|hook-after-edit.mjs|hook-stop.mjs`, then appends fresh); invalid existing JSON → error `Existing hook manifest is not valid JSON: ${dest}. Re-run with --force to replace it.` (with `--force`, `.bak` written). Hook command rewriting: `[ ! -f '<abs>' ] || node '<abs>'` (POSIX, single-quote-escaped) when absolute (user scope or global skill), else `[ ! -f "<rel>" ] || node "<rel>"` where rel = `${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs` (Claude), `.agents/skills/impeccable/scripts/hook.mjs` (Codex; plus `commandWindows: if exist "<p>" (node "<p>" & exit /b)`), `.cursor/skills/impeccable/scripts/hook-before-edit.mjs`; on win32 for non-Codex: `node -e "<WIN32_HOOK_GUARD_SCRIPT>" "<path>"`. Output: `Installed impeccable into: ${targets.join(', ')} (${'global'|'project'})`, optional `Installed <Provider> agents into: <path>` (+ shadow warning), `Installed hooks into: ...`, then `\nDone! Now type /impeccable init in your AI coding agent's chat (not in this terminal) to set up design context.\n`. Errors: `Download failed: ...` / `Install failed: ...` / `Nothing was installed: the bundle had no variants for ...` → exit 1.
|
||||
Already installed (and not `--force`): `Impeccable skills are already installed (found in ${provider}/).`; compares tree hashes (`sha256` of file content with `\.(claude|cursor|...)\/skills\/` normalized to `.PROVIDER/skills/`); if differs → refresh + `Updated ${n} skill(s) to v${v}.`; missing hooks repaired; else `Skills are up to date (v${v}).` + `Run with --force to reinstall.`; offline → `Could not check for skill updates: ${msg}` + `Existing skills were left unchanged.`; ends `Done!` or the above; `exit 0`. Version read from `^version:\s*(.+)$` in installed `impeccable/SKILL.md`.
|
||||
|
||||
@@ -162,6 +162,7 @@ The skill-behavior suite runs three providers (claude-haiku-4-5, gpt-5.4-mini, g
|
||||
- [HARNESSES.md](HARNESSES.md) - Provider capabilities matrix
|
||||
- [Cursor Skills](https://cursor.com/docs/context/skills)
|
||||
- [Claude Code Skills](https://code.claude.com/docs/en/skills)
|
||||
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
||||
- [Gemini CLI Skills](https://geminicli.com/docs/cli/skills/)
|
||||
- [Codex CLI Skills](https://developers.openai.com/codex/skills/)
|
||||
- [VS Code Copilot Skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
|
||||
|
||||
+20
-17
@@ -3,7 +3,7 @@
|
||||
Source of truth for what each AI coding harness supports in terms of agent skills.
|
||||
Used to inform provider configs in `scripts/lib/transformers/providers.js`.
|
||||
|
||||
Last verified: 2026-04-28 (subagent landscape spot-checked 2026-06-28; Mistral Vibe row verified 2026-07-16; Grok Build skills row verified 2026-07-21; Grok Build hook stdin captured 2026-08-24)
|
||||
Last verified: 2026-04-28 (subagent landscape spot-checked 2026-06-28; Mistral Vibe row verified 2026-07-16; Grok Build skills row verified 2026-07-21; Grok Build hook stdin captured 2026-08-24; DeepSeek Harness row verified 2026-09-06)
|
||||
|
||||
> This file is point-in-time. Capabilities move fast; verify live before relying
|
||||
> on any "only X supports Y" claim. Notably, the subagent table below lists
|
||||
@@ -15,6 +15,7 @@ Last verified: 2026-04-28 (subagent landscape spot-checked 2026-06-28; Mistral V
|
||||
|---------|----------|
|
||||
| Claude Code | https://code.claude.com/docs/en/skills |
|
||||
| Cursor | https://cursor.com/docs/context/skills |
|
||||
| DeepSeek Harness | https://github.com/deepseek-ai/deepseek-harness |
|
||||
| Gemini CLI | https://geminicli.com/docs/cli/skills/ |
|
||||
| Codex CLI | https://developers.openai.com/codex/skills |
|
||||
| GitHub Copilot (Agents) | https://code.visualstudio.com/docs/copilot/customization/agent-skills |
|
||||
@@ -39,22 +40,22 @@ Provider-specific extensions beyond the spec: `user-invocable`, `argument-hint`,
|
||||
|
||||
Fields marked with * are spec-standard. Others are provider extensions.
|
||||
|
||||
| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Grok | Hermes | Kiro | OpenCode | Pi | Qoder | Rovo Dev | Mistral Vibe | Antigravity |
|
||||
|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:------:|:----:|:--------:|:--:|:-----:|:--------:|:------------:|:-----------:|
|
||||
| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `allowed-tools`* | Yes | No | Ignored | No | No | Yes | No | No | No | Yes | Yes | Yes | Yes | Yes |
|
||||
| `user-invocable` | Yes | No | No | No | Yes | Yes | No | No | No | No | Yes | Yes | Yes | No |
|
||||
| `argument-hint` | Yes | No | No | No | Yes | Yes | No | No | No | No | Yes | Yes | No | No |
|
||||
| `disable-model-invocation` | Yes | Yes | No | No | Yes | Yes | No | No | Yes | Yes | TBD | TBD | No | No |
|
||||
| `model` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No | No |
|
||||
| `effort` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No | No |
|
||||
| `context` | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
||||
| `agent` | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
||||
| `hooks` | Yes | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No |
|
||||
| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Grok | Hermes | Kiro | OpenCode | Pi | Qoder | Rovo Dev | Mistral Vibe | Antigravity | DSH |
|
||||
|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:------:|:----:|:--------:|:--:|:-----:|:--------:|:------------:|:-----------:|:------:|
|
||||
| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Ignored |
|
||||
| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Ignored |
|
||||
| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| `allowed-tools`* | Yes | No | Ignored | No | No | Yes | No | No | No | Yes | Yes | Yes | Yes | Yes | No |
|
||||
| `user-invocable` | Yes | No | No | No | Yes | Yes | No | No | No | No | Yes | Yes | Yes | No | Yes |
|
||||
| `argument-hint` | Yes | No | No | No | Yes | Yes | No | No | No | No | Yes | Yes | No | No | No |
|
||||
| `disable-model-invocation` | Yes | Yes | No | No | Yes | Yes | No | No | Yes | Yes | TBD | TBD | No | No | Yes |
|
||||
| `model` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No | No | No |
|
||||
| `effort` | Yes | No | No | No | No | Yes | No | No | No | No | No | No | No | No | No |
|
||||
| `context` | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
||||
| `agent` | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
||||
| `hooks` | Yes | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No | No |
|
||||
|
||||
Notes:
|
||||
- Gemini CLI validates only `name` and `description`; other spec fields are parsed but ignored.
|
||||
@@ -65,6 +66,7 @@ Notes:
|
||||
- Kiro recognizes `user-invocable` and `disable-model-invocation` per community reports but does not formally document them.
|
||||
- Antigravity supports standard Agent Skills spec frontmatter fields (`name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`).
|
||||
- OpenCode 1.18.10 recognises only the spec subset on SKILL.md (`name`, `description`, `license`, `compatibility`, `metadata`). Claude-style extensions (`user-invocable`, `argument-hint`, `allowed-tools`, `model`, `agent`) are silently ignored; Impeccable still emits them today for other harnesses, but they have no effect in OpenCode. Use `commands/<name>.md` (see Placeholder / Variable Substitution below) for slash UX; OpenCode honours only `description`, `agent`, `model`, `variant`, `subtask` on command files.
|
||||
- DeepSeek Harness parses the Agent Skills frontmatter and requires `name` and `description`; it reads `metadata`, `user-invocable`, and `disable-model-invocation`. Spec fields it does not consume (`license`, `compatibility`, `allowed-tools`) and Claude-style extensions (`argument-hint`, `model`, `effort`, `context`, `agent`, `hooks`) are silently ignored. Hooks are in-process plugins configured via cordis.yml, not on-disk manifests, so there is no hook surface to install. Subagents exist but are composed from preset config, not an on-disk skill-adjacent format. Verified against the [filesystem skill provider](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/skill/skill-filesystem/README.md).
|
||||
- Unknown fields are silently ignored by all harnesses.
|
||||
|
||||
## Hook surface used by Impeccable
|
||||
@@ -83,6 +85,7 @@ Notes:
|
||||
|---------|-----------------|------------|
|
||||
| Claude Code | `.claude/skills/` | - |
|
||||
| Cursor | `.cursor/skills/` | `.agents/skills/`, `.claude/skills/` |
|
||||
| DeepSeek Harness | `.dsh/skills/` (project), `~/.dsh/skills/` (global; `$DSH_HOME/skills` when set) | `.agents/skills/` (project), `~/.agents/skills/` (global) |
|
||||
| Gemini CLI | `.gemini/skills/` | `.agents/skills/` |
|
||||
| Codex CLI | `.agents/skills/` (primary) | - |
|
||||
| GitHub Copilot | `.github/skills/` | `.agents/skills/`, `.claude/skills/` |
|
||||
|
||||
@@ -641,6 +641,7 @@ This folder contains skills for all supported tools:
|
||||
|
||||
.cursor/ -> Cursor
|
||||
.claude/ -> Claude Code
|
||||
.dsh/ -> DeepSeek Harness
|
||||
.gemini/ -> Gemini CLI
|
||||
.codex/ -> Codex custom agents (Codex skills use .agents/)
|
||||
.agents/ -> Codex CLI
|
||||
|
||||
@@ -44,6 +44,20 @@ export const PROVIDERS = {
|
||||
displayName: 'Gemini',
|
||||
frontmatterFields: [],
|
||||
},
|
||||
dsh: {
|
||||
provider: 'dsh',
|
||||
providerTags: ['dsh'],
|
||||
configDir: '.dsh',
|
||||
displayName: 'DeepSeek Harness',
|
||||
// DeepSeek Harness reads the Agent Skills spec subset (`name`,
|
||||
// `description`, `license`, `compatibility`, `metadata`) plus
|
||||
// `user-invocable` and `disable-model-invocation`; unknown keys are
|
||||
// silently ignored. No hook surface (hooks are in-process plugins, not
|
||||
// on-disk manifests) and no native subagent file format, so no
|
||||
// emitHooks / agentFormat. Global skills live at ~/.dsh/skills
|
||||
// ($DSH_HOME/skills when set), matching the engine's home override.
|
||||
frontmatterFields: ['user-invocable', 'license', 'compatibility', 'metadata'],
|
||||
},
|
||||
codex: {
|
||||
provider: 'codex',
|
||||
providerTags: ['codex'],
|
||||
|
||||
@@ -424,6 +424,12 @@ export const PROVIDER_PLACEHOLDERS = {
|
||||
ask_instruction: 'Ask the user directly to clarify what you cannot infer.',
|
||||
command_prefix: '/'
|
||||
},
|
||||
'dsh': {
|
||||
model: 'DeepSeek',
|
||||
config_file: 'AGENTS.md',
|
||||
ask_instruction: 'STOP and call the ask_user_question tool to clarify.',
|
||||
command_prefix: '/'
|
||||
},
|
||||
'gemini': {
|
||||
model: 'Gemini',
|
||||
config_file: 'GEMINI.md',
|
||||
@@ -524,6 +530,7 @@ export const PROVIDER_BLOCK_TAGS = new Set([
|
||||
'claude-code',
|
||||
'codex',
|
||||
'cursor',
|
||||
'dsh',
|
||||
'gemini',
|
||||
'github',
|
||||
'grok',
|
||||
|
||||
@@ -65,6 +65,29 @@ for (const [key, config] of Object.entries(PROVIDERS)) {
|
||||
expect(fs.existsSync(refPath)).toBe(true);
|
||||
});
|
||||
|
||||
if (key === 'dsh') {
|
||||
test('uses DSH tools and resource paths without installing unsupported hooks or agents', () => {
|
||||
transform([{
|
||||
name: 'impeccable',
|
||||
description: 'Test',
|
||||
userInvocable: true,
|
||||
allowedTools: 'Bash',
|
||||
body: '{{ask_instruction}} Run `{{scripts_path}}/impeccable context`.',
|
||||
references: [{ name: 'polish', content: 'Run `{{scripts_path}}/impeccable detect`.', filePath: '/fake/polish.md' }],
|
||||
}], TEST_DIR);
|
||||
const content = fs.readFileSync(skillPath(config, 'impeccable'), 'utf8');
|
||||
expect(content).toContain('call the ask_user_question tool');
|
||||
expect(content).toContain('.dsh/skills/impeccable/scripts/impeccable context');
|
||||
expect(parseFrontmatter(content).frontmatter['allowed-tools']).toBeUndefined();
|
||||
const root = path.join(TEST_DIR, 'dsh', '.dsh');
|
||||
expect(fs.readFileSync(path.join(root, 'skills/impeccable/reference/polish.md'), 'utf8'))
|
||||
.toContain('.dsh/skills/impeccable/scripts/impeccable detect');
|
||||
expect(fs.readdirSync(root)).toEqual(['skills']);
|
||||
expect(config.emitHooks).toBeUndefined();
|
||||
expect(config.agentFormat).toBeUndefined();
|
||||
});
|
||||
}
|
||||
|
||||
test('should emit skillsVersion in generated skill frontmatter', () => {
|
||||
const skills = [{ name: 'test', description: 'Test', body: 'Body' }];
|
||||
transform(skills, TEST_DIR, { skillsVersion: '1.2.3-test' });
|
||||
|
||||
Reference in New Issue
Block a user