Skip to content

feat: scan project-level subagents across 6 agents#42

Merged
RealZST merged 5 commits intomainfrom
feat/project-subagent-scanning-core
May 8, 2026
Merged

feat: scan project-level subagents across 6 agents#42
RealZST merged 5 commits intomainfrom
feat/project-subagent-scanning-core

Conversation

@RealZST
Copy link
Copy Markdown
Owner

@RealZST RealZST commented May 8, 2026

Summary

  • New Subagents ConfigCategory: 6 adapters (claude/codex/cursor/gemini/copilot/opencode) scan their agents/* directories at both global and project scope; previously only global was partially covered (under Settings) and project was completely invisible.
  • Behavior change for existing users: ~/.{agent}/agents/*.md files visually relocate from Settings section to a new Subagents section in the agent detail panel. Codex's ~/.codex/agents/*.toml is scanned for the first time.
  • Bug fix: Copilot filter tightened from *.md to require *.agent.md naming, matching the documented Copilot CLI convention. Anchor-rail "Workflow" label normalized to "Workflows" for consistency with the section header.

Scope per adapter

Agent Global subagent path Project subagent path
claude ~/.claude/agents/*.md (was Settings) .claude/agents/*.md (NEW)
codex ~/.codex/agents/*.toml (NEW) .codex/agents/*.toml (NEW)
cursor ~/.cursor/agents/*.md (was Settings) .cursor/agents/*.md (NEW)
gemini ~/.gemini/agents/*.md (was Settings) .gemini/agents/*.md (NEW)
copilot ~/.copilot/agents/*.agent.md (was Settings, filter tightened) .github/agents/*.agent.md (NEW)
opencode ~/.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:

  1. feat(core) β€” ConfigCategory::Subagents variant + trait methods (global_subagent_files / project_subagent_patterns) with empty defaults + scanner integration
  2. feat(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 tests
  3. refactor(adapter) β€” extract pub(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.md stem filter chain freely without double allocation.
  4. feat(ui) β€” wire ConfigCategory::Subagents through frontend (TypeScript union, label record, render order, icon=Bot, anchor rail). Without this, backend-tagged subagent files would be silently dropped.
  5. refactor β€” dedupe pre-existing category-table duplication: introduce CONFIG_CATEGORY_ORDER as single source of truth (was duplicated between agent-detail.tsx and section-anchor-rail.tsx); add impl FromStr for ConfigCategory with round-trip parity test, eliminating identical 6-arm parsers in hk-web/hk-desktop.

Test coverage

  • Backend (385 tests pass): 6 adapter unit tests covering empty/happy/wrong-ext/no-leak invariants per agent; 2 scanner integration tests covering Claude .md (with global+project scope assertions) and Codex .toml; round-trip parity test for as_str() ↔ FromStr; default-impl test extended to cover the 2 new trait methods.
  • Frontend (148 tests pass, tsc clean): no test added β€” pre-existing 5 categories don't have render-level tests; adding only for subagents would be inconsistent. Type-system parity (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:

  • Global: created code-reviewer.{md,toml,agent.md} in each adapter's user-scope agents/ dir (~/.claude/agents/, ~/.codex/agents/, ~/.cursor/agents/, ~/.gemini/agents/, ~/.copilot/agents/, ~/.config/opencode/agents/)
  • Project: created planner.{md,toml,agent.md} in /private/tmp/hk-subagent-test/.{agent}/agents/ (Copilot at .github/agents/)
  • Copilot filter regression: stray ~/.copilot/agents/note.md (no .agent stem) confirmed NOT to appear under Subagents
  • All fixtures cleaned up post-test

Verified visually for all 6 adapters:

  • Subagents section renders with Bot icon at correct position (between Rules and Memory)
  • Files appear in their respective scope (Global / Project)
  • Codex's ~/.codex/agents/*.toml visible for the first time (was completely unscanned before this PR)
  • 5 adapters' agents/*.md correctly migrated out of Settings into Subagents
  • Anchor rail shows clickable "Subagents" link; "Workflow" label now "Workflows"
  • Section collapse/expand state persists across reload
  • No regression in Settings/Workflow/Rules/Memory/Ignore sections

Test plan

  • CI passes on all platforms
  • Local: existing user with ~/.claude/agents/foo.md opens HK β†’ file appears under Subagents in Claude detail panel (Global scope), not Settings
  • Local: project with .claude/agents/bar.md opens HK β†’ file appears under Subagents (Project scope)
  • Local: Codex user with ~/.codex/agents/baz.toml opens HK β†’ first-time appearance under Codex Subagents
  • Local: Copilot user with ~/.copilot/agents/foo.agent.md opens HK β†’ appears under Subagents; a stray note.md (no .agent stem) does NOT appear (intended tightening)
  • Anchor rail shows "Subagents" link, click jumps to the section
  • Section collapse/expand state persists across reload (existing localStorage behavior for new category)
  • No regression in existing Settings/Workflow/Rules/Memory/Ignore sections

Roadmap follow-ups (not in this PR)

  • project_adapter_readdir_helper_extract_roadmap.md β€” 5 pre-existing same-shape read_dir + ext filter loops elsewhere (claude commands/output-styles, gemini commands/policies, copilot hooks, codex memories) can migrate to files_with_ext in 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

RealZST and others added 5 commits May 8, 2026 15:07
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>
@RealZST RealZST merged commit b69a469 into main May 8, 2026
3 checks passed
@RealZST RealZST deleted the feat/project-subagent-scanning-core branch May 8, 2026 16:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant