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.
# 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
+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
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.
+13
View File
@@ -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)
+2 -2
View File
@@ -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"];
+1
View File
@@ -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",
+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(|| {
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
View File
@@ -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).
+34
View File
@@ -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"])]);
}
+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).
- `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`.
+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
- [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
View File
@@ -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/` |
+1
View File
@@ -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
+14
View File
@@ -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'],
+7
View File
@@ -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',
+23
View File
@@ -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' });