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:
Sheece Gardezi
2026-09-06 14:46:05 -07:00
committed by GitHub
co-authored by Paul Bakaus
parent f41300b3b2
commit 6ebc24ad66
17 changed files with 306 additions and 34 deletions
+1 -1
View File
@@ -137,7 +137,7 @@ jobs:
# What `bun run build` writes, which runs on every PR. # What `bun run build` writes, which runs on every PR.
# extension/detector/ is gitignored (built by `cargo xtask bundle`); # extension/detector/ is gitignored (built by `cargo xtask bundle`);
# it stays listed so a stray tracked copy shows up here. # 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 - name: Upload build artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
@@ -26,6 +26,7 @@ env:
.codex .codex
.claude .claude
.cursor .cursor
.dsh
.gemini .gemini
.github/agents .github/agents
.github/hooks .github/hooks
+1 -1
View File
@@ -24,7 +24,7 @@ Run `bun run build` after changing anything in `skill/`, transformer code, or us
## Generated Provider Output Policy ## 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. 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.
+13
View File
@@ -184,6 +184,18 @@ cp -r dist/claude-code/.claude/* ~/.claude/
cp -r dist/opencode/.opencode your-project/ 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:** **Hermes Agent:**
```bash ```bash
# Global (applies to all projects; uses the active profile, or ~/.hermes by default) # 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) - [Cursor](https://cursor.com)
- [Claude Code](https://claude.ai/code) - [Claude Code](https://claude.ai/code)
- [GitHub Copilot](https://github.com/features/copilot) - [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) - [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- [Codex CLI](https://github.com/openai/codex) - [Codex CLI](https://github.com/openai/codex)
- [Grok Build](https://x.ai/cli) - [Grok Build](https://x.ai/cli)
+2 -2
View File
@@ -8,8 +8,8 @@ use impeccable_common::Io;
/// keep in sync with the public repo). /// keep in sync with the public repo).
pub const COMMAND_METADATA_JSON: &str = include_str!("command-metadata.json"); pub const COMMAND_METADATA_JSON: &str = include_str!("command-metadata.json");
const HARNESS_DIRS: [&str; 17] = [ const HARNESS_DIRS: [&str; 18] = [
".claude", ".cursor", ".gemini", ".codex", ".agents", ".agent", ".github", ".grok", ".hermes", ".trae", ".trae-cn", ".claude", ".cursor", ".dsh", ".gemini", ".codex", ".agents", ".agent", ".github", ".grok", ".hermes", ".trae", ".trae-cn",
".pi", ".opencode", ".kiro", ".rovodev", ".vibe", ".qoder", ".pi", ".opencode", ".kiro", ".rovodev", ".vibe", ".qoder",
]; ];
const CODEX_HARNESSES: [&str; 2] = [".codex", ".agents"]; const CODEX_HARNESSES: [&str; 2] = [".codex", ".agents"];
+1
View File
@@ -62,6 +62,7 @@ fn provider_from_skill_dir(skill_dir: &str) -> Option<&'static str> {
Some(match harness.as_str() { Some(match harness.as_str() {
".claude" => "claude-code", ".claude" => "claude-code",
".cursor" => "cursor", ".cursor" => "cursor",
".dsh" => "dsh",
".gemini" => "gemini", ".gemini" => "gemini",
".codex" => "codex", ".codex" => "codex",
".agents" => "agents", ".agents" => "agents",
+1 -1
View File
@@ -405,7 +405,7 @@ pub fn list_skill_tree_files(root: &str) -> Vec<String> {
} }
static PROVIDER_PATH_RE: Lazy<Regex> = Lazy::new(|| { 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) /// JS: normalizeForHash(content)
+107 -10
View File
@@ -8,7 +8,7 @@ use crate::util::{self, jsp, Env};
pub const API_BASE: &str = "https://impeccable.style"; pub const API_BASE: &str = "https://impeccable.style";
pub const PROVIDER_DIRS: &[&str] = &[ 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", ".opencode", ".pi", ".qoder", ".trae", ".trae-cn", ".rovodev", ".vibe",
]; ];
@@ -21,6 +21,9 @@ const PROVIDER_ALIASES: &[(&str, &str)] = &[
("codex", ".agents"), ("codex", ".agents"),
("copilot", ".github"), ("copilot", ".github"),
("cursor", ".cursor"), ("cursor", ".cursor"),
("deepseek", ".dsh"),
("deepseek-harness", ".dsh"),
("dsh", ".dsh"),
("gemini", ".gemini"), ("gemini", ".gemini"),
("github", ".github"), ("github", ".github"),
("grok", ".grok"), ("grok", ".grok"),
@@ -43,6 +46,7 @@ const PROVIDER_DISPLAY: &[(&str, &str, &str)] = &[
(".agents", "Codex CLI", "codex"), (".agents", "Codex CLI", "codex"),
(".claude", "Claude Code", "claude"), (".claude", "Claude Code", "claude"),
(".cursor", "Cursor", "cursor"), (".cursor", "Cursor", "cursor"),
(".dsh", "DeepSeek Harness", "dsh"),
(".gemini", "Gemini CLI", "gemini"), (".gemini", "Gemini CLI", "gemini"),
(".github", "GitHub Copilot", "github"), (".github", "GitHub Copilot", "github"),
(".grok", "Grok Build", "grok"), (".grok", "Grok Build", "grok"),
@@ -58,7 +62,7 @@ const PROVIDER_DISPLAY: &[(&str, &str, &str)] = &[
]; ];
pub const PROVIDER_INPUT_ORDER: &[&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", "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"]) 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) /// JS: HOME_SKILLS_DIR_OVERRIDES[provider]?.(home)
fn home_skills_dir_override(env: &Env, cwd: &str, provider: &str, home: &str) -> Option<String> { fn home_skills_dir_override(env: &Env, cwd: &str, provider: &str, home: &str) -> Option<String> {
match provider { match provider {
".agent" => Some(jsp::join(&[home, ".gemini", "config", "skills"])), ".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"])), ".hermes" => Some(jsp::join(&[&hermes_global_home(env, cwd, home), "skills"])),
".pi" => Some(jsp::join(&[home, ".pi", "agent", "skills"])), ".pi" => Some(jsp::join(&[home, ".pi", "agent", "skills"])),
".opencode" => Some(jsp::join(&[&opencode_global_config_dir(env, home), "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 { 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 /// 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"])), 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 { for hint in GLOBAL_HARNESS_HINTS {
let (found_path, probe_paths) = match hint { let (found_path, probe_paths) = match hint {
Hint::Home(rel, provider) => { Hint::Home(rel, provider) => {
@@ -320,12 +354,10 @@ impl Sys {
(found, probes) (found, probes)
} }
Hint::OpencodeConfig(provider) => { Hint::OpencodeConfig(provider) => {
let found = opencode_global_config_dir(&self.env, home); config_dir_detection(provider, opencode_global_config_dir(&self.env, home))
let probes = unique_paths(vec![ }
self.user_provider_skills_dir(home, provider), Hint::DshHome(provider) => {
jsp::join(&[&found, "skills"]), config_dir_detection(provider, dsh_global_home(&self.env, &self.cwd, home))
]);
(found, probes)
} }
}; };
if !util::exists(&found_path) { if !util::exists(&found_path) {
@@ -415,12 +447,16 @@ pub enum UpdateTarget {
enum Hint { enum Hint {
Home(&'static str, &'static str), Home(&'static str, &'static str),
OpencodeConfig(&'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 { impl Hint {
fn provider(&self) -> &'static str { fn provider(&self) -> &'static str {
match self { 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(".claude", ".claude"),
Hint::Home(".codex", ".agents"), Hint::Home(".codex", ".agents"),
Hint::Home(".cursor", ".cursor"), Hint::Home(".cursor", ".cursor"),
Hint::DshHome(".dsh"),
Hint::Home(".gemini", ".gemini"), Hint::Home(".gemini", ".gemini"),
Hint::Home(".grok", ".grok"), Hint::Home(".grok", ".grok"),
Hint::Home(".hermes", ".hermes"), Hint::Home(".hermes", ".hermes"),
@@ -758,6 +795,66 @@ mod tests {
assert_eq!(invalid, vec!["zzz"]); 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] #[test]
fn version_extraction() { fn version_extraction() {
// Values recorded from origin/main's parseSkillFrontmatterVersion (#703). // Values recorded from origin/main's parseSkillFrontmatterVersion (#703).
+34
View File
@@ -397,6 +397,40 @@ fn claude_install_and_update_backfill_bundled_agents() {
std::fs::remove_dir_all(&root).ok(); 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) ────────────────────────────────── // ─── home-scoped agent freshness (16a218e6) ──────────────────────────────────
#[test] #[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"])]);
}
+2 -2
View File
@@ -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). - **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`. - `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: - **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`. sorted by `id.localeCompare`.
- **Flag parsing** (`getFlagValue`): `--name=value` or `--name value` (next arg not starting with `-`). Boolean flags via `includes`. - **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`. 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. 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`. 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`.
+1
View File
@@ -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 - [HARNESSES.md](HARNESSES.md) - Provider capabilities matrix
- [Cursor Skills](https://cursor.com/docs/context/skills) - [Cursor Skills](https://cursor.com/docs/context/skills)
- [Claude Code Skills](https://code.claude.com/docs/en/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/) - [Gemini CLI Skills](https://geminicli.com/docs/cli/skills/)
- [Codex CLI Skills](https://developers.openai.com/codex/skills/) - [Codex CLI Skills](https://developers.openai.com/codex/skills/)
- [VS Code Copilot Skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills) - [VS Code Copilot Skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
+20 -17
View File
@@ -3,7 +3,7 @@
Source of truth for what each AI coding harness supports in terms of agent skills. 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`. 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 > 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 > 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 | | Claude Code | https://code.claude.com/docs/en/skills |
| Cursor | https://cursor.com/docs/context/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/ | | Gemini CLI | https://geminicli.com/docs/cli/skills/ |
| Codex CLI | https://developers.openai.com/codex/skills | | Codex CLI | https://developers.openai.com/codex/skills |
| GitHub Copilot (Agents) | https://code.visualstudio.com/docs/copilot/customization/agent-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. 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 | | 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `hooks` | Yes | No | No | Yes | No | Yes | No | No | No | No | No | No | No | No | No |
Notes: Notes:
- Gemini CLI validates only `name` and `description`; other spec fields are parsed but ignored. - 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. - 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`). - 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. - 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. - Unknown fields are silently ignored by all harnesses.
## Hook surface used by Impeccable ## Hook surface used by Impeccable
@@ -83,6 +85,7 @@ Notes:
|---------|-----------------|------------| |---------|-----------------|------------|
| Claude Code | `.claude/skills/` | - | | Claude Code | `.claude/skills/` | - |
| Cursor | `.cursor/skills/` | `.agents/skills/`, `.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/` | | Gemini CLI | `.gemini/skills/` | `.agents/skills/` |
| Codex CLI | `.agents/skills/` (primary) | - | | Codex CLI | `.agents/skills/` (primary) | - |
| GitHub Copilot | `.github/skills/` | `.agents/skills/`, `.claude/skills/` | | GitHub Copilot | `.github/skills/` | `.agents/skills/`, `.claude/skills/` |
+1
View File
@@ -641,6 +641,7 @@ This folder contains skills for all supported tools:
.cursor/ -> Cursor .cursor/ -> Cursor
.claude/ -> Claude Code .claude/ -> Claude Code
.dsh/ -> DeepSeek Harness
.gemini/ -> Gemini CLI .gemini/ -> Gemini CLI
.codex/ -> Codex custom agents (Codex skills use .agents/) .codex/ -> Codex custom agents (Codex skills use .agents/)
.agents/ -> Codex CLI .agents/ -> Codex CLI
+14
View File
@@ -44,6 +44,20 @@ export const PROVIDERS = {
displayName: 'Gemini', displayName: 'Gemini',
frontmatterFields: [], 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: { codex: {
provider: 'codex', provider: 'codex',
providerTags: ['codex'], providerTags: ['codex'],
+7
View File
@@ -424,6 +424,12 @@ export const PROVIDER_PLACEHOLDERS = {
ask_instruction: 'Ask the user directly to clarify what you cannot infer.', ask_instruction: 'Ask the user directly to clarify what you cannot infer.',
command_prefix: '/' command_prefix: '/'
}, },
'dsh': {
model: 'DeepSeek',
config_file: 'AGENTS.md',
ask_instruction: 'STOP and call the ask_user_question tool to clarify.',
command_prefix: '/'
},
'gemini': { 'gemini': {
model: 'Gemini', model: 'Gemini',
config_file: 'GEMINI.md', config_file: 'GEMINI.md',
@@ -524,6 +530,7 @@ export const PROVIDER_BLOCK_TAGS = new Set([
'claude-code', 'claude-code',
'codex', 'codex',
'cursor', 'cursor',
'dsh',
'gemini', 'gemini',
'github', 'github',
'grok', 'grok',
+23
View File
@@ -65,6 +65,29 @@ for (const [key, config] of Object.entries(PROVIDERS)) {
expect(fs.existsSync(refPath)).toBe(true); 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', () => { test('should emit skillsVersion in generated skill frontmatter', () => {
const skills = [{ name: 'test', description: 'Test', body: 'Body' }]; const skills = [{ name: 'test', description: 'Test', body: 'Body' }];
transform(skills, TEST_DIR, { skillsVersion: '1.2.3-test' }); transform(skills, TEST_DIR, { skillsVersion: '1.2.3-test' });