AGENTS.md + config.yaml + optional SOUL.md/MEMORY.md. The backend (loader, reconcile, IPC, migration, D9 filter) is production-quality. The user-facing CRUD UI is not finished.
Agent system at a glance
Folder layout
readAgentWorkspace(dir) returns null if AGENTS.md is missing OR config.yaml is missing OR config.yaml lacks name + description (loader.mjs:217-240). Subdirectories that fail this check are silently skipped.
Slug validation: ^[a-z0-9_-]{1,64}$, no leading dot, no path separators (agents.rs:258-281).
config.yaml fields
| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | non-empty after trim |
description | yes | string | non-empty after trim |
avatar_emoji / avatarEmoji | no | string | snake + camelCase accepted |
provider | no | string | default 'claude_sdk' |
model | no | string | e.g. claude-sonnet-4-6 |
tools | no | string[] or csv | null = inherit all |
color | no | string | UI hint |
thinking | no | enum | off/minimal/low/medium/high/xhigh |
fallback_models | no | string[] | provider fallback chain |
output | no | string | default output file |
default_reads | no | string[] | files to pre-read |
default_progress | no | bool | progress event default |
max_subagent_depth | no | int | recursive nesting cap |
is_archived is NOT a config.yaml field — it lives only in the runtime DB. See Three representations below.Real example: prompts/agents/general/
Two-layer override (D4)
| Layer | Path | Mutability | Provenance label |
|---|---|---|---|
| Built-in | prompts/agents/<slug>/ | Read-only; refreshed by app upgrades; is_builtin_source_path() blocks CRUD disk writes | sourceLayer: 'builtin' |
| Vault | <vault>/agents/<slug>/ | User-editable; travels with vault sync | sourceLayer: 'vault' |
loader.mjs:470-487):
- Scan
builtinDir→ seed Map withbySlug.set(slug, {…, sourceLayer:'builtin'}) - Scan
vaultDir→ overwrite withbySlug.set(slug, {…, sourceLayer:'vault'}) - Sort + return
AgentWorkspace (including AGENTS.md/config.yaml/SOUL.md/MEMORY.md) comes from the vault. No file-level merge across layers. ✓ VERIFIED.
CRUD guard for built-ins (agents.rs:611-616) — update_agent rejects disk writes for built-ins:
“Cannot modify built-in agent’s content. To customize a built-in agent, create a vault entry with the same slug.”
delete_agent enforces the same. Runtime patches (is_archived, custom_env, …) ARE allowed for built-ins because they live only in the DB.
Three representations
| Representation | Truth for | Read | Write |
|---|---|---|---|
| Filesystem folder | AUTHORITATIVE — body, slug, every config.yaml field | resolveAgentLayered({builtinDir, vaultDir}) | Edit config.yaml atomically via agents.rs::atomic_write |
| Build-time TS registry | Static name / slug / description / tools — SYNC cold-load | import { AGENT_REGISTRY } from '@/lib/agents/generated-agent-registry' | node scripts/generate-agent-registry.mjs (NEVER hand-edit) |
| Runtime DB index | is_archived, custom_env, custom_args, mcp_config, max_concurrent_runs + cached provider/model/name/description | list_agents IPC → useAgents() hook | update_agent / archive_agent IPC |
initLoader.mjs:128-136):
D9 subagent body filter — enforcement across paths
| Dispatch path | File:line | Status |
|---|---|---|
Claude SDK Agent/Task | claude-agents-map.mjs:102 | ✓ assembleAgentBody(ws, {isSubagent:true}) applied unconditionally to every entry in query({agents:{…}}) map |
Pi spawn_subagent | pi-subagent-runner.mjs:934 | ✓ getAgentDefForSubagent(agentName, options.agentsDir) |
Pi agent_info (discovery) | pi-tools.mjs:1421-1424 | ✓ POST VAL-009 fix (commit d2a26640, 2026-05-05) — was leaking SOUL+MEMORY before |
| Top-level chat (agentSlug) | loader.mjs:516-546 | ✓ resolveTopLevelAgentBody → isSubagent:false (full body) |
loader.mjs:303-307:
assembleAgentBody({isSubagent:true}) OR leak SOUL+MEMORY to downstream models.
agent_info) MUST also use getAgentDefForSubagent — not getAgentDef. The contract applies to the exposure surface, not just the dispatch surface. Verified leak path was fixed at commit d2a26640.Migration paths
- Build-time (D11)
- Runtime (D11)
scripts/migrate-bundled-agents.mjs converts prompts/agents/<slug>.md → prompts/agents/<slug>/{AGENTS.md, config.yaml}. Archives originals to prompts/agents/.legacy/<slug>.md.- Idempotent: re-running is no-op when folder +
AGENTS.md+config.yamlexist;--forceoverwrites - Flags:
--dry-run,--verbose,--force,--dir <path> - Wired into
prebuildchain - Rollback:
mv prompts/agents/.legacy/* prompts/agents/then delete the empty folder dirs - ✓ COMPLETE: 34 agent folders present at
prompts/agents/, no live.mdfiles
CRUD round-trip flow
Persistence guarantees (✓ VERIFIED):- Content fields round-trip through
config.yaml. Even if reconcile runs, disk YAML is authoritative forname/description/model/tools/.... - Runtime fields stay in the DB —
ON CONFLICT(id) DO UPDATE SETomitsis_archived,max_concurrent_runs, andcustom_env / custom_args / mcp_configare never written by reconcile. - Atomic writes:
tempfile::NamedTempFile::new_in(parent) + persist(target)— rename(2) POSIX, MoveFileEx Windows. Crash mid-write leaves previous file intact. - Delete safety: rusqlite tx with
fs::remove_dir_allAFTERDELETEbut BEFOREcommit. FS failure drops tx → ROLLBACK → DB row preserved. - Archive cache invalidation: every CRUD broadcast triggers
invalidateArchiveCache()BEFORE reconcile, so subagent spawns see fresh archive flags within sub-second of mutation.
What works today
✓ Browse 34 bundled agents
AgentsManager.tsx renders the bundled agents from AGENT_REGISTRY (build-time static).✓ Search + filter
✓ Toggle enabled/disabled
toggleAgent(slug, enabled) via the agent-manifest path (NOT is_archived).✓ Click-through to detail
agent-detail page → CapabilityDetail (read-only).✓ Bundled agent migration on boot
.md files in vault auto-convert to folder layout, with backup.✓ Backend CRUD IPCs tested
agents.rs covering slug validation, config.yaml round-trip, atomic write, runtime patches, archive toggling, built-in protection.Honest gaps
GAP-AG1: No agent CRUD UI wired (CRITICAL)
GAP-AG1: No agent CRUD UI wired (CRITICAL)
src/lib/ipc/agents.ts exports createAgent, updateAgent, archiveAgent, deleteAgent with full TypeScript types. grep -rn 'createAgent|updateAgent|archiveAgent|deleteAgent' src/ returns only the export site, zero call sites.A new component (modal, drawer, or full editor page) must be built that:- Submits to
createAgentfrom the ”+ New Agent” button - Submits to
updateAgentfrom a future edit form inCapabilityDetail - Calls
archiveAgentfrom a confirm dialog - Calls
deleteAgentfrom a destructive confirm
GAP-AG2: AgentsManager doesn't see vault agents
GAP-AG2: AgentsManager doesn't see vault agents
resourceConfigStore.ts:133-141 builds managedAgents from AGENT_REGISTRY ONLY. Vault-created agents would never appear because the registry is regenerated only from prompts/agents/<slug>/config.yaml at build time.Fix: replace the AGENT_REGISTRY.map(...) block with await listAgents() OR migrate the component to call useAgents() directly.GAP-AG3: "+ New Agent" button is a dead button
GAP-AG3: "+ New Agent" button is a dead button
GAP-AG4: CapabilityDetail.tsx is read-only
GAP-AG4: CapabilityDetail.tsx is read-only
GAP-AG5: Two parallel "disabled agent" concepts
GAP-AG5: Two parallel "disabled agent" concepts
disabledAgents manifest (read by getAgentManifest() + ipcToggleAgent at resourceConfigStore.ts:96,178) is NOT the same as agents.is_archived.The Pi/Claude dispatch paths check is_archived via archive-cache.mjs, filtering subagent dispatch via claude-agents-map.mjs:94 and pi-subagent-runner.mjs. The UI toggle does NOT update the column subagent dispatch checks.Concrete consequence: Toggling a bundled agent “Off” in the UI does NOT prevent it from being spawned as a subagent. Only the new archive_agent IPC does, and nothing calls it.GAP-AG6: No frontend listener for migration event
GAP-AG6: No frontend listener for migration event
orion://agents/migrated is window-broadcast (routing.rs:540) but grep -rn 'orion://agents/migrated' src/ returns zero. Toast or any user feedback is silently dropped.Either wire a toast or remove the broadcast path.GAP-AG7: agentsReloaded cache invalidation is dead code in practice
GAP-AG7: agentsReloaded cache invalidation is dead code in practice
useAgents subscribes to orion://agents/reloaded, but the hook has no consumers, so reconcile events have no visible effect on the UI.GAP-AG8: Legacy _legacy_db_sync_loader.mjs (441 LOC)
GAP-AG8: Legacy _legacy_db_sync_loader.mjs (441 LOC)
destructive-commands.md rule — needs user approval before rm).GAP-AG9: generated-agent-registry.ts stale
GAP-AG9: generated-agent-registry.ts stale
config.yaml has been edited since 2026-05-05, sync renderers (mention autocomplete, slash commands) show stale descriptions.Fix: run node scripts/generate-agent-registry.mjs before each release.Open questions
Q1: Who owns the UX design for agent authoring?
Q1: Who owns the UX design for agent authoring?
Q2: Will "disabled agents manifest" be deprecated for is_archived?
Q2: Will "disabled agents manifest" be deprecated for is_archived?
Q3: useAgents() to replace AGENT_REGISTRY in resourceConfigStore?
Q3: useAgents() to replace AGENT_REGISTRY in resourceConfigStore?
Q4: Where in UI shell should migration toast mount?
Q4: Where in UI shell should migration toast mount?
<Toaster> / <Sonner> mount was confirmed in this audit.Q5: MEMORY.md v2 (dynamic, write-during-session)?
Q5: MEMORY.md v2 (dynamic, write-during-session)?
Key files
src-tauri/sidecar/agents/loader.mjs (546 LOC)
src-tauri/sidecar/agents/loader.mjs (546 LOC)
resolveAgentLayered (L470-487), loadAgent, assembleAgentBody, filterBootstrapFilesForSession (L303-307 — D9 ALLOWLIST), resolveTopLevelAgentBody (L516-546). ✓ VERIFIED complete.src-tauri/sidecar/agents/initLoader.mjs (315 LOC)
src-tauri/sidecar/agents/initLoader.mjs (315 LOC)
ON CONFLICT(id) DO UPDATE SET clause at L128-136 deliberately preserves runtime cols. ✓ VERIFIED complete.src-tauri/sidecar/agents/claude-agents-map.mjs (124 LOC)
src-tauri/sidecar/agents/claude-agents-map.mjs (124 LOC)
query({agents:{...}}) map. Applies D9 unconditionally at L102. ✓ VERIFIED.src-tauri/sidecar/agents/archive-cache.mjs (207 LOC)
src-tauri/sidecar/agents/archive-cache.mjs (207 LOC)
AgentArchivedError thrown at spawn time. ✓ VERIFIED.src-tauri/sidecar/agents/runtime-migration.mjs (348 LOC)
src-tauri/sidecar/agents/runtime-migration.mjs (348 LOC)
<vault>/.orion/agents/*.md → folder layout migrator. Backup-before-write, atomic per-file, rollback documented. ✓ VERIFIED complete.src-tauri/src/commands/agents.rs (1215 LOC)
src-tauri/src/commands/agents.rs (1215 LOC)
src/lib/ipc/agents.ts (240 LOC)
src/lib/ipc/agents.ts (240 LOC)
src/).src/hooks/useAgents.ts (191 LOC)
src/hooks/useAgents.ts (191 LOC)
orion://agents/reloaded subscription. ✓ implemented, ✗ zero consumers.src/components/resources/agents/AgentsManager.tsx (196 LOC)
src/components/resources/agents/AgentsManager.tsx (196 LOC)
AGENT_REGISTRY (build-time) via resourceConfigStore, NOT useAgents(). ”+ New Agent” button has no onClick. ✗ PARTIAL: read-only.src/components/resources/shared/CapabilityDetail.tsx (243 LOC)
src/components/resources/shared/CapabilityDetail.tsx (243 LOC)