feat: scan project-level subagents across 6 agents#42
Merged
Conversation
Add a new ConfigCategory::Subagents variant (ordered between Memory and Settings) and matching AgentAdapter trait methods global_subagent_files() and project_subagent_patterns(). Wire them into scan_agent_configs so any adapter that overrides them gets its subagent definition files scanned into the new category β the trait defaults stay vec![] so adapters with no subagent concept (e.g. windsurf, antigravity) need no changes. This commit is the structural foundation; per-adapter implementations (claude/codex/cursor/gemini/copilot/opencode) come in a follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement the new global_subagent_files() / project_subagent_patterns()
trait methods for claude, codex, cursor, gemini, copilot, and opencode.
Adds project-level subagent file discovery (.{agent}/agents/) which was
previously missing for all 6, and closes Codex's global-side gap (it
never scanned ~/.codex/agents/).
Five adapters (claude/cursor/gemini/copilot/opencode) previously misclassified
their global agents/ directory under Settings. This commit moves those scans
into the new Subagents category β a behavior change visible to users as
agent persona files relocating from the Settings section to a new Subagents
section. opencode's existing test was updated to reflect the move.
Also tightens copilot's filter from extension == "md" to require a *.agent.md
naming, matching the documented Copilot CLI convention. Plain .md notes left
in ~/.copilot/agents/ no longer get misclassified as subagents.
Two parsers (hk-web/handlers/agents.rs, hk-desktop/commands/agents.rs)
accept the new "subagents" string in custom-config-path categorization.
Tests: 6 adapter unit tests covering empty/happy/wrong-ext/no-leak
contracts, plus 2 scanner integration tests (Claude .md, Codex .toml).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote OpenCode's previously-private `Self::files_with_ext` to a module-level `pub(crate) fn files_with_ext(dir, ext) -> impl Iterator<...>` in adapter/mod.rs. All 6 adapters' new `global_subagent_files` use it, and OpenCode's 4 existing callsites migrate too β eliminating duplication of the read_dir + extension-filter pattern in subagent paths. Returning `impl Iterator` (not Vec) lets callers chain extra predicates without paying a second allocation. Copilot benefits directly: its `*.agent.md` stem filter is now a single `.filter(...).collect()` instead of `.into_iter().filter(...).collect()` on a pre-materialized Vec. Simple callers add one `.collect()`; `Vec::extend` callers work unchanged (extend accepts IntoIterator). Pre-existing same-shape loops elsewhere (claude commands/output-styles, gemini commands/policies, copilot hooks, codex memories) are intentionally left for a separate cleanup PR β tracked in roadmap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the new "subagents" category through the frontend so the backend's ConfigCategory::Subagents files (claude/codex/cursor/gemini/copilot/opencode agent persona definitions) actually render in the UI. Without this commit agents/*.md files would be silently dropped after backend migration β agent-detail.tsx:84 only includes files whose category appears in CATEGORY_ORDER. Five touch points across four files: TypeScript union, label record, render order, icon map, anchor rail catalog. All purely declarative β the existing render pipeline iterates these tables, so adding entries auto-wires the new section without logic changes. Position: subagents sits between rules and memory in both CATEGORY_ORDER and SECTION_CATALOG β semantically adjacent to rules (both define agent behavior; memory is accumulated state). Icon: lucide-react Bot. Closes the end-to-end feature originally split as a separate PR2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small refactors that close pre-existing duplication this PR's earlier commits perpetuated rather than introduced. Frontend: introduce CONFIG_CATEGORY_ORDER as the single source of truth for ConfigCategory render order. agent-detail.tsx and section-anchor-rail.tsx both now derive from it (the rail composes ids/labels via map). Side benefit: rail label "Workflow" β "Workflows", aligning with the section header (which already used CONFIG_CATEGORY_LABELS). Backend: add `impl FromStr for ConfigCategory` plus a round-trip parity test (`as_str().parse() == Ok(self)` for every variant + unknown-string negative). hk-web and hk-desktop custom-config parsers replace their identical 6-arm match with `.parse().unwrap_or(Settings)`. The Err policy is documented inline so callsite-level fallback intent stays explicit. Net: -34 / +70 lines (most of the +70 is the FromStr impl + parity test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
RealZST
added a commit
that referenced
this pull request
May 8, 2026
* refactor(adapter): dedupe remaining read_dir loops with files_with_ext Follow-up to PR #42's helper extraction. Replaces 8 pre-existing `read_dir + extension filter` loops with `super::files_with_ext` helper calls; behaviour-preserving mechanical refactor, 4 adapters touched: - claude.rs: rules/, commands/, output-styles/ (3 loops in global_rules_files / global_settings_files); inner memory/ loop in global_memory_files (outer projects/* iteration kept β multi-level pattern doesn't match the helper) - gemini.rs: commands/, policies/ in global_settings_files - copilot.rs: hooks/ in global_settings_files - codex.rs: memories/ in global_memory_files β adds explicit `.filter(|p| p.is_file())` to preserve prior `is_file()` check (cheap parity over a "siblings don't bother" argument) Net: -64 / +12 lines. 385 hk-core tests pass, clippy -D warnings clean. Plugin/marketplace recursive multi-level scans intentionally left untouched β they have a different shape than the helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lint): drive biome to clean (0 errors / 0 warnings / 0 info) Reduces biome diagnostics from 13 errors + 12 warnings + 1 info to fully clean. Combines auto-fix and manual passes: Auto-fix (Step 2 β `npm run lint:fix`, 8 files): import sorting, formatter pass, blank-line cleanup. Pure cosmetic, zero behavior change. Manual (Step 3, by category): Category A β trivial mechanical (2 sites): - transport.ts useLiteralKeys: `headers["Authorization"]` β `.Authorization` - invoke.ts useOptionalChain: `!value || !value.trim()` β `!value?.trim()` Category D β noExplicitAny (1 site, onboarding.tsx): - Removed dead `roughness` field from rough-notation `annotate()` config (library hardcodes its own roughness via getOptions(type) in render.js and never reads user-supplied values), removed accompanying `as any`. Category C β useExhaustiveDependencies (4 sites, 5 findings): - Added single-line `biome-ignore` with rationale at each site where the dep array intentionally omits a value or includes a trigger sentinel: - section-anchor-rail revisionKey (documented re-discovery sentinel) - extension-filters extensions (Zustand getter trigger) - extension-table scope (cell-renderer getState() trigger) - scope-switcher-menu handleSelect/handleAddProject (new closures capturing only stable refs β adding them would only churn the keydown listener) - Replaced obsolete `eslint-disable-next-line` comment in scope-switcher-menu (project uses biome, not eslint). Category B β noNonNullAssertion (9 sites, 5 biome-ignore + 4 refactor): - biome-ignore where the `!` reflects a TS narrowing limitation through JSX/by-construction guarantees (main.tsx React entry, config-file-entry JSX gates, detail-paths Map.get of own keys, test idiom). - Refactor where a small rewrite removes the `!` cleanly: - delete-dialog: redundant `&& filteredSkillLocations` lets TS narrow - extension-detail: extracted `activePath` local var (kills 2 `!`) - new-skills-dialog: explicit Map get-or-create binding - agent-config-store: single `get()` + undefined check instead of has()/get()! pair (cache stores strings only, so `cached !== undefined` is exactly equivalent to `has`) Final state: `npm run lint` clean; tsc clean; 148/148 frontend tests pass; no behavior changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
agents/*directories at both global and project scope; previously only global was partially covered (under Settings) and project was completely invisible.~/.{agent}/agents/*.mdfiles visually relocate from Settings section to a new Subagents section in the agent detail panel. Codex's~/.codex/agents/*.tomlis scanned for the first time.*.mdto require*.agent.mdnaming, matching the documented Copilot CLI convention. Anchor-rail "Workflow" label normalized to "Workflows" for consistency with the section header.Scope per adapter
~/.claude/agents/*.md(was Settings).claude/agents/*.md(NEW)~/.codex/agents/*.toml(NEW).codex/agents/*.toml(NEW)~/.cursor/agents/*.md(was Settings).cursor/agents/*.md(NEW)~/.gemini/agents/*.md(was Settings).gemini/agents/*.md(NEW)~/.copilot/agents/*.agent.md(was Settings, filter tightened).github/agents/*.agent.md(NEW)~/.config/opencode/agents/*.md(was Settings).opencode/agents/*.md(NEW)windsurf and antigravity have no native subagent concept β left untouched.
Architecture
5 commits, each independently bisect-able:
feat(core)β ConfigCategory::Subagents variant + trait methods (global_subagent_files/project_subagent_patterns) with empty defaults + scanner integrationfeat(adapters)β 6 adapter impls, OpenCode test updated for category move, parsers in hk-web/hk-desktop accept"subagents"string, 6 adapter unit tests, 2 scanner integration testsrefactor(adapter)β extractpub(crate) fn files_with_ext(dir, ext) -> impl Iterator<Item = PathBuf>to adapter/mod.rs (was OpenCode-private). All 6 subagent paths use it; OpenCode's 4 existing callsites migrated. Iterator return lets Copilot's*.agent.mdstem filter chain freely without double allocation.feat(ui)β wireConfigCategory::Subagentsthrough frontend (TypeScript union, label record, render order, icon=Bot, anchor rail). Without this, backend-tagged subagent files would be silently dropped.refactorβ dedupe pre-existing category-table duplication: introduceCONFIG_CATEGORY_ORDERas single source of truth (was duplicated between agent-detail.tsx and section-anchor-rail.tsx); addimpl FromStr for ConfigCategorywith round-trip parity test, eliminating identical 6-arm parsers in hk-web/hk-desktop.Test coverage
.md(with global+project scope assertions) and Codex.toml; round-trip parity test foras_str() β FromStr; default-impl test extended to cover the 2 new trait methods.Record<ConfigCategory, ...>) enforces table completeness at compile time.Manual testing β all 6 adapters verified end-to-end
β Hands-on UI tested for every adapter at both Global and Project scope using temporary fixture files:
code-reviewer.{md,toml,agent.md}in each adapter's user-scopeagents/dir (~/.claude/agents/,~/.codex/agents/,~/.cursor/agents/,~/.gemini/agents/,~/.copilot/agents/,~/.config/opencode/agents/)planner.{md,toml,agent.md}in/private/tmp/hk-subagent-test/.{agent}/agents/(Copilot at.github/agents/)~/.copilot/agents/note.md(no.agentstem) confirmed NOT to appear under SubagentsVerified visually for all 6 adapters:
~/.codex/agents/*.tomlvisible for the first time (was completely unscanned before this PR)agents/*.mdcorrectly migrated out of Settings into SubagentsTest plan
~/.claude/agents/foo.mdopens HK β file appears under Subagents in Claude detail panel (Global scope), not Settings.claude/agents/bar.mdopens HK β file appears under Subagents (Project scope)~/.codex/agents/baz.tomlopens HK β first-time appearance under Codex Subagents~/.copilot/agents/foo.agent.mdopens HK β appears under Subagents; a straynote.md(no.agentstem) does NOT appear (intended tightening)Roadmap follow-ups (not in this PR)
project_adapter_readdir_helper_extract_roadmap.mdβ 5 pre-existing same-shaperead_dir + ext filterloops elsewhere (claude commands/output-styles, gemini commands/policies, copilot hooks, codex memories) can migrate tofiles_with_extin a separate cleanup PR (~-50 lines)project_opencode_jsonc_dual_file_merge_roadmap.mdβ unrelated, prior P2 follow-up to PR feat(opencode): support jsonc configs with comment preservationΒ #41