Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7717fc2
feat(scope): add useScopeStore + useScope hook + URL/localStorage sync
RealZST Apr 30, 2026
255a09a
fix(scope): gate scope hydration on project-store load completion
RealZST Apr 30, 2026
e352b58
feat(scope): add Sidebar ScopeSwitcher component + dropdown
RealZST Apr 30, 2026
eacf800
feat(scope): add ScopeBadge shared component
RealZST Apr 30, 2026
e66b2fe
refactor(extensions): replace local scopeFilter with useScope hook
RealZST Apr 30, 2026
91519e4
refactor(agents): replace per-agent scope pills with useScope hook
RealZST Apr 30, 2026
a262fbd
feat(audit): scope-aware filtering on Audit page
RealZST Apr 30, 2026
c5e8aca
feat(adapter): add skill_dir_for + declare project_skill_dirs on all …
RealZST Apr 30, 2026
940e8f0
feat(install): make post_install_sync scope-aware (1/3)
RealZST Apr 30, 2026
4cd41d8
feat(install): thread target_scope through web + desktop handlers (2/3)
RealZST Apr 30, 2026
9ed7fc1
feat(install): thread targetScope through frontend invoke + stores (3/3)
RealZST Apr 30, 2026
90e0f0f
feat(install): scope-aware install dialogs + project-scope deploy gate
RealZST Apr 30, 2026
ad69c90
feat(scope): handle edge cases (invalid URL, project removal, hydration)
RealZST Apr 30, 2026
6ec80cd
feat(scope): scope-aware empty states on Extensions/Agents/Audit
RealZST Apr 30, 2026
453d723
chore(scope): a11y polish on switcher and dialogs
RealZST Apr 30, 2026
1d6431f
feat(scope): scope-aware Add Custom Path with DB v4 migration
RealZST May 1, 2026
5dcbc82
fix(scope): include HK-installed project skills in Check Updates + si…
RealZST May 1, 2026
45259f0
refactor(scope): drop per-row ScopeBadge for detail-panel scope info …
RealZST May 1, 2026
85fa94d
fix(panels): collapse Agents file previews on scope/page change + res…
RealZST May 1, 2026
a6b7eb6
feat(install): scope picker for NewSkillsDialog + Extensions panel hy…
RealZST May 1, 2026
05ac559
refactor(scope): re-style Sidebar ScopeSwitcher to match nav links + …
RealZST May 1, 2026
4200d25
feat(toast): add warning variant + reclassify scope/skip messages fro…
RealZST May 1, 2026
5097c58
chore: satisfy clippy + biome formatter for branch changes
RealZST May 1, 2026
94636ff
chore: apply biome formatter to repo (whitespace + line-wrap only)
RealZST May 1, 2026
e61120c
fix(scope): Overview deep links carry scope through to Agents/Extensions
RealZST May 1, 2026
ca64c48
refactor(scope): clean up deep-link handling in Agents/Extensions
RealZST May 1, 2026
a36bc28
fix(scope-target-field): drop stray bullet before single-scope hint
RealZST May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
docs/
.superpowers/
.claude/
.worktrees/
branding/
.impeccable.md
.env*
Expand Down
6 changes: 6 additions & 0 deletions crates/hk-core/src/adapter/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ impl AgentAdapter for AntigravityAdapter {
self.home.join(".gemini").join("antigravity").join("skills"),
]
}
fn project_skill_dirs(&self) -> Vec<String> {
// Antigravity workspace skills.
// SINGULAR ".agent/skills" (Antigravity convention) — NOT ".agents/skills".
// Source: https://codelabs.developers.google.com/getting-started-with-antigravity-skills
vec![".agent/skills".into()]
}
fn mcp_config_path(&self) -> PathBuf {
self.home
.join(".gemini")
Expand Down
6 changes: 6 additions & 0 deletions crates/hk-core/src/adapter/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ impl AgentAdapter for CodexAdapter {
vec![".codex/config.toml".into()]
}

fn project_skill_dirs(&self) -> Vec<String> {
// Codex CLI scans .agents/skills from cwd up to the repo root.
// Source: https://developers.openai.com/codex/skills
vec![".agents/skills".into()]
}

fn read_mcp_servers(&self) -> Vec<McpServerEntry> {
self.read_mcp_servers_from(&self.mcp_config_path())
}
Expand Down
8 changes: 8 additions & 0 deletions crates/hk-core/src/adapter/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ impl AgentAdapter for CopilotAdapter {
self.home.join(".agents").join("skills"),
]
}
fn project_skill_dirs(&self) -> Vec<String> {
// GitHub Copilot Agent Skills (canonical path .github/skills; also accepts
// .claude/skills and .agents/skills aliases — declaring only the canonical
// here keeps scanner output deduplicated when other adapters declare those
// alias paths).
// Source: https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills
vec![".github/skills".into()]
}
fn mcp_config_path(&self) -> PathBuf {
self.vscode_user_dir().join("mcp.json")
}
Expand Down
6 changes: 6 additions & 0 deletions crates/hk-core/src/adapter/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ impl AgentAdapter for CursorAdapter {
]
}

fn project_skill_dirs(&self) -> Vec<String> {
// Cursor 2.4+ project skills.
// Source: https://cursor.com/docs/skills
vec![".cursor/skills".into()]
}

fn mcp_config_path(&self) -> PathBuf {
self.base_dir().join("mcp.json")
}
Expand Down
7 changes: 7 additions & 0 deletions crates/hk-core/src/adapter/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ impl AgentAdapter for GeminiAdapter {
self.home.join(".agents").join("skills"),
]
}
fn project_skill_dirs(&self) -> Vec<String> {
// Gemini CLI workspace skills (also accepts .agents/skills as an alias —
// declaring only the canonical path here; cross-agent shared skills via
// .agents/skills are picked up by the Codex adapter naturally).
// Source: https://geminicli.com/docs/cli/skills/
vec![".gemini/skills".into()]
}
fn mcp_config_path(&self) -> PathBuf {
self.base_dir().join("settings.json")
}
Expand Down
92 changes: 92 additions & 0 deletions crates/hk-core/src/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ pub trait AgentAdapter: Send + Sync {
.map(|rel| std::path::Path::new(path).join(rel)),
}
}

/// Resolve the skill directory for a given scope.
/// - `Global` → first entry of `skill_dirs()` (today's behavior).
/// - `Project` → `<project>/<project_skill_dirs()[0]>`, or `None` if
/// the adapter has no project-level skill support.
fn skill_dir_for(&self, scope: &ConfigScope) -> Option<std::path::PathBuf> {
match scope {
ConfigScope::Global => self.skill_dirs().into_iter().next(),
ConfigScope::Project { path, .. } => self
.project_skill_dirs()
.into_iter()
.next()
.map(|rel| std::path::Path::new(path).join(rel)),
}
}
}

/// Returns all agent adapters in canonical display order.
Expand Down Expand Up @@ -299,4 +314,81 @@ mod tests {
let _ = a.project_workflow_patterns();
}
}

#[test]
fn test_skill_dir_for_global_matches_skill_dirs_first() {
let adapters = all_adapters();
for a in &adapters {
let global = ConfigScope::Global;
let computed = a.skill_dir_for(&global);
let expected = a.skill_dirs().into_iter().next();
assert_eq!(
computed,
expected,
"{} skill_dir_for(Global) should match skill_dirs()[0]",
a.name()
);
}
}

#[test]
fn test_skill_dir_for_project_joins_path_with_project_skill_dirs_first() {
let adapters = all_adapters();
let scope = ConfigScope::Project {
name: "demo".into(),
path: "/tmp/demo".into(),
};
for adapter in &adapters {
let computed = adapter.skill_dir_for(&scope);
let rel = adapter.project_skill_dirs().into_iter().next();
match (&computed, &rel) {
(Some(p), Some(r)) => {
assert_eq!(p, &std::path::Path::new("/tmp/demo").join(r));
}
(None, None) => {} // adapter has no project skill support
_ => panic!(
"{}: mismatched some/none: computed={computed:?} vs project_skill_dirs first={rel:?}",
adapter.name()
),
}
}
}

#[test]
fn test_every_adapter_declares_project_skill_dir() {
// Universal Agent Skills standard (Dec 2025) — every adapter must declare a
// project skill directory. If a future adapter genuinely has no project
// skill concept, drop it from this assertion explicitly.
let adapters = all_adapters();
for a in &adapters {
assert!(
!a.project_skill_dirs().is_empty(),
"{} must declare project_skill_dirs (Universal Agent Skills standard)",
a.name()
);
}
}

#[test]
fn test_project_skill_dir_paths_match_upstream_conventions() {
// Verify each adapter's first-party documented path. Update when adapter
// upstream conventions change.
let adapters = all_adapters();
let expected: std::collections::HashMap<&str, &str> = [
("claude", ".claude/skills"),
("codex", ".agents/skills"), // Universal alias adopted by OpenAI
("cursor", ".cursor/skills"),
("windsurf", ".windsurf/skills"),
("gemini", ".gemini/skills"),
("antigravity", ".agent/skills"), // Singular — Antigravity convention
("copilot", ".github/skills"),
]
.into_iter()
.collect();
for a in &adapters {
let actual = a.project_skill_dirs().into_iter().next().unwrap();
let want = expected.get(a.name()).expect("adapter not in expected map");
assert_eq!(&actual, want, "{} project skill path mismatch", a.name());
}
}
}
6 changes: 6 additions & 0 deletions crates/hk-core/src/adapter/windsurf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ impl AgentAdapter for WindsurfAdapter {
]
}

fn project_skill_dirs(&self) -> Vec<String> {
// Windsurf Cascade workspace skills.
// Source: https://docs.windsurf.com/windsurf/cascade/skills
vec![".windsurf/skills".into()]
}

fn mcp_config_path(&self) -> PathBuf {
self.base_dir().join("mcp_config.json")
}
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ mod tests {

#[test]
fn test_from_io_error_other() {
let err = std::io::Error::new(std::io::ErrorKind::Other, "something else");
let err = std::io::Error::other("something else");
let hk: HkError = err.into();
assert!(matches!(hk, HkError::Internal(_)));
}
Expand Down
Loading
Loading