Skip to content

feat: modularize agent config into .agents/ directory with safe file persistence#1107

Closed
aj47 wants to merge 18 commits into
mainfrom
feat/agents-modular-config
Closed

feat: modularize agent config into .agents/ directory with safe file persistence#1107
aj47 wants to merge 18 commits into
mainfrom
feat/agents-modular-config

Conversation

@aj47

@aj47 aj47 commented Feb 23, 2026

Copy link
Copy Markdown
Owner

Summary

Introduce a canonical .agents/ directory structure for storing agent configuration as discrete, workspace-shareable files with simple key: value frontmatter. This enables:

  • Workspace sharing.agents/ can be committed to repos and shared across agent frameworks
  • Safe agent self-modification — atomic writes, timestamped backups, auto-recovery
  • Global + workspace overlay — workspace .agents/ overrides global by ID

New Infrastructure

apps/desktop/src/main/agents-files/

File Purpose
frontmatter.ts Simple key: value frontmatter parser/serializer (no YAML dependency)
safe-file.ts Atomic writes (temp+rename), timestamped backups with rotation, auto-recovery from backup on JSON parse failure
modular-config.ts AgentsLayerPaths type, global+workspace overlay semantics, config splitting/merging
memories.ts Memory .md file read/write with frontmatter metadata
skills.ts Skill .md file read/write, directory scanning, backup support
*.test.ts Full test coverage for all modules (5 test files)

Skills Migration

  • SkillsService now loads/writes skills from .agents/skills/**/skill.md
  • Global + workspace overlay (workspace overrides global by ID)
  • Legacy skills.json auto-migrated on first load (non-destructive)
  • All CRUD operations use atomic writes with backup + rollback on failure
  • initializeBundledSkills() copies to .agents/skills/ instead of legacy folder
  • File watcher watches .agents/skills dirs (global + workspace)
  • openSkillsFolder IPC opens .agents/skills directory

Memory Service Migration

  • MemoryService now uses .agents/memories/*.md as canonical storage
  • Global + workspace overlay semantics

Config Updates

  • config.ts exports globalAgentsFolder and resolveWorkspaceAgentsFolder()
  • System prompts refactored to support modular config

Verification

  • ✅ TypeScript compilation (tsconfig.node.json) — passes clean
  • ✅ All 111 unit tests pass across 14 test files
  • ✅ All existing method signatures preserved (no breaking API changes)

Directory Structure

.agents/
├── skills/
│   ├── my-skill/
│   │   └── skill.md          # frontmatter: id, name, description, enabled
│   └── another-skill/
│       └── skill.md
├── memories/
│   └── memory-name.md         # frontmatter: id, content summary
├── .backups/                  # timestamped backups (auto-rotated)
│   ├── skills/
│   └── memories/
└── (future: mcp.json, models.json, speakmcp-settings.json, etc.)

Pull Request opened by Augment Code with guidance from the PR author

aj47 added 2 commits February 22, 2026 19:56
…persistence

Introduce a canonical .agents/ directory structure for storing agent configuration
as discrete, workspace-shareable files with simple key:value frontmatter.

## New infrastructure (apps/desktop/src/main/agents-files/)

- frontmatter.ts: Simple key:value frontmatter parser/serializer (no YAML dep)
- safe-file.ts: Atomic writes (temp+rename), timestamped backups with rotation,
  auto-recovery from backup on JSON parse failure
- modular-config.ts: AgentsLayerPaths type, global+workspace overlay semantics,
  config splitting/merging utilities
- memories.ts: Memory .md file read/write with frontmatter metadata
- skills.ts: Skill .md file read/write, directory scanning, backup support
- Full test coverage for all modules (5 test files)

## Skills migration to .agents canonical storage

- SkillsService now loads/writes skills from .agents/skills/**/skill.md
- Global + workspace overlay (workspace overrides global by ID)
- Legacy skills.json auto-migrated on first load (non-destructive)
- All CRUD operations use atomic writes with backup+rollback
- initializeBundledSkills() copies to .agents/skills/ instead of legacy folder
- File watcher watches .agents/skills dirs (global + workspace)
- openSkillsFolder IPC opens .agents/skills directory

## Memory service migration

- MemoryService now uses .agents/memories/*.md as canonical storage
- Global + workspace overlay semantics

## Config updates

- config.ts exports globalAgentsFolder and resolveWorkspaceAgentsFolder()
- System prompts refactored to support modular config

All existing method signatures preserved. TypeScript compilation and all 111
unit tests pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e55e3f6270

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +361 to +364
writeAgentsLayerFromConfig(globalLayer, this.config, DEFAULT_SYSTEM_PROMPT, {
// Avoid rewriting user-managed files on startup; only create missing files.
onlyIfMissing: true,
maxBackups: 10,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop seeding global config from merged workspace state

getConfig() already returns a global+workspace merged object, so writing this.config into the global .agents layer here copies workspace overrides into global files whenever those global files are missing. In a workspace with project-specific settings (or keys), simply launching the app can silently leak those values into global config and break the intended overlay isolation across projects.

Useful? React with 👍 / 👎.

Comment on lines +660 to +663
try {
fs.unlinkSync(filePath)
} catch {
// best-effort

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate unlink failures when deleting skill files

Swallowing fs.unlinkSync errors makes deleteSkill() report success even when the wrapper file could not be removed (for example, Windows file locks/permission issues). Because the file remains on disk, the skill can reappear after reload/restart, so users get a false-success deletion result and inconsistent state.

Useful? React with 👍 / 👎.

Previously, getConfig() skipped loading config.json entirely when .agents
files existed. This caused settings loss (API keys, preferences) because
the .agents files were seeded with defaults on first run, then treated as
the exclusive source on subsequent runs.

Now the merge order is: defaults ← config.json ← .agents (if present).
This ensures existing config.json settings are always preserved while
.agents files can selectively override specific values.
@augmentcode

augmentcode Bot commented Feb 23, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Modularizes agent configuration and content into a canonical .agents/ directory with safer, workspace-shareable persistence.

Changes:

  • Added agents-files/ utilities for frontmatter parsing, atomic writes, timestamped backups/rotation, and JSON recovery.
  • Introduced layered config loading/merging (global + optional workspace overlay) and writing config back into split .agents files.
  • Migrated Skills persistence to .agents/skills/**/skill.md with legacy skills.json migration and updated folder watching.
  • Migrated Memory persistence to .agents/memories/*.md with one-time import from legacy memories.json.
  • Updated config load/save flow to prefer .agents while keeping legacy config.json updated for compatibility.
  • Extracted DEFAULT_SYSTEM_PROMPT into an import-free module to avoid circular dependencies.
  • Added Vitest coverage for the new persistence/config modules.

Technical Notes: Workspace layer overrides global by ID; writes are atomic and keep backup snapshots for recovery.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

}
const { globalLayer, workspaceLayer } = this.getLayers()

const targetLayerName = previousOrigin?.layer ?? "global"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

targetLayerName can remain "workspace" even when workspaceLayer is null, so the write goes to the global layer but originById records it as workspace. Consider deriving the stored layer name from the actual targetLayer chosen to keep future saves/deletes consistently routed.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if (rootWatcher) skillsWatchers.push(rootWatcher)

try {
const entries = fs.readdirSync(dir, { withFileTypes: true })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux, refreshLinuxSubdirectoryWatchers() only adds watchers for the immediate children of each skills root, but skills are now allowed in nested paths (.agents/skills/**/skill.md). Changes under deeper nested folders likely won’t be detected, so the renderer may not refresh when nested skills are added/edited.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

aj47 added 15 commits February 22, 2026 21:20
Phase 3: Migrate all main process callers from profileService → agentProfileService
- tipc.ts: All ~56 profileService references migrated
  - Session snapshots (2 locations)
  - Profile CRUD, setCurrentProfile, export/import
  - MCP/model config handlers
  - Skill auto-enable + management handlers
  - Memory handlers
- builtin-tools.ts: All 7 tool handlers migrated
- remote-server.ts: All REST API endpoints migrated
- llm.ts, mcp-service.ts, loop-service.ts: Already migrated

Phase 4: IPC handler consolidation
- Legacy handlers (getProfiles, etc.) now call agentProfileService
- Unified handlers (getAgentProfiles, etc.) remain canonical
- No duplicate logic - legacy handlers use backward-compat wrappers

Phase 5: Merge UI
- New settings-agents.tsx with tabbed view (User Profiles, Personas, External Agents)
- Updated router.tsx with redirects from old paths
- Updated app-layout.tsx sidebar (2 entries → 1 'Agents' entry)
- Updated sidebar-profile-selector.tsx to use unified types

REST API: Added ?role= filter to /v1/agent-profiles endpoint
Legacy /v1/profiles endpoints kept as aliases for backward compat
…stem

- Remove user profiles concept entirely; settings are now global
- Rebrand 'Persona' to 'Agent' in all user-facing text and comments
- Remove 7 profile built-in tools (list/switch/get/create/update/delete/duplicate)
- Remove profile management UI from settings-tools, settings-loops, settings-skills
- Rewrite settings-agents.tsx as flat Agents list (no more 3-tab view)
- Remove sidebar profile selector and dead files (profile-manager, settings-agent-personas, settings-external-agents)
- Fix memory system: remove profile requirement from delete operations
- Fix memories page: remove profile badge and profile-scoped queries
- Fix LLM memory loading: use getAllMemories() instead of profile-filtered
- Fix getCurrentProfile() fallback to find any default agent
- Rename personaProperties → agentProperties across types, llm, system-prompts
- Rename getAgentPersonasPromptAddition → getAgentsPromptAddition
- Update prompt text: 'AVAILABLE AGENT PERSONAS' → 'AVAILABLE AGENTS'
…ies tabs

- Add tabbed edit form: General, Behavior, Model, Tools, Skills, Properties
- Model tab: LLM provider selector (openai/groq/gemini) with model picker
- Tools tab: per-agent MCP server enable/disable with opt-in/opt-out mode
- Skills tab: per-agent skill toggles from available skills
- Properties tab: key-value editor for dynamic system prompt variables
- Enhanced agent list cards with server/skill/property count badges
- All config saved via updateAgentProfile IPC handler
… concept

- Rename built-in 'General Assistant' to 'Main Agent' as the primary default agent
- Remove legacy 'Default' user-profile entry from DEFAULT_PROFILES
- Main Agent gets isDefault: true and owns .agents/agents.md guidelines
- Update setCurrentProfile to accept any agent (not just user-profiles)
- Update importProfile and createUserProfile to create delegation-targets
- Use Main Agent for skills config in non-agent mode (llm.ts)
- Remove isUserProfile filter from settings-agents.tsx
- Remove legacy profile cleanup: profile-badge.tsx deleted, ~200 lines of
  per-profile UI logic removed from mcp-config-manager, settings-skills,
  predefined-prompts-menu, settings-mcp-tools, settings-providers
- Update DEBUGGING.md with two-protocol documentation
Adds an AGENT RESOURCES section to AGENT_MODE_ADDITIONS that teaches
every agent how to discover, manage, and use their resources:

- Tools: list/explore/inspect/toggle MCP servers and tools
- Skills: load instructions, create new skills, run in skill dirs
- Memories: save/list/delete persistent cross-session knowledge
- Loops: understand scheduled recurring agent tasks
…llback

- internal-agent.ts: Remove duplicate createProfileSnapshotFromAgentProfile(),
  use canonical createSessionSnapshotFromProfile() from agent-profile-service.ts.
  Fixes: allServersDisabledByDefault defaulting to true (broke Main Agent delegation),
  profileId prefix with 'agent_', and missing profileName fallback.

- agent-profile-service.ts: getCurrentProfile() now falls back to isBuiltIn
  profile when no isDefault or currentProfileId is set. Handles migrated data
  where isDefault wasn't propagated from DEFAULT_PROFILES.
…ools call

The previous architecture fired two parallel LLM API calls on every agent
iteration:
- makeLLMCallWithStreaming (streamText, no tools) — display only
- makeLLMCallWithFetch (generateText, with tools) — actual tool execution

Because these were independent API calls to the same model, they could
produce different outputs due to non-determinism. The user would see one
plan streaming in (e.g. 'I'll call load_skill_instructions') while a
completely different tool actually executed (e.g. iterm:list_sessions).

Fix: replace both calls with a single makeLLMCallWithStreamingAndTools
function that uses streamText() with tools enabled. Iterating fullStream
gives real-time text-delta events for display and tool-call events for
execution from the same model response — divergence is structurally
impossible.

The old makeLLMCallWithStreaming export is preserved for compatibility
but is no longer used in the agent loop.
- Install facehash package for deterministic client-side avatar generation
- Redesign agents list as a card grid with facehash avatars
- Add avatarDataUrl field to AgentProfile type for custom photo storage
- Generate unique per-agent color palettes via djb2 hash over curated palette
- Add avatar upload UI in agent edit form (General tab) with remove option
- Fall back to colored facehash when no custom photo is set
- Store custom avatars as base64 data URLs (CSP-safe, no external requests)
Two fixes in getAvailableToolsForProfile() in mcp-service.ts:

1. Treat empty enabledBuiltinTools array same as undefined (no whitelist).
   Legacy profiles had enabledBuiltinTools: [] which JS treated as truthy,
   causing the filter to reject all non-essential builtins.

2. Apply disabledTools filtering only to external MCP tools, not builtins.
   Builtin availability is controlled by enabledBuiltinTools whitelist.
   Legacy profiles had all builtin names in disabledTools, which
   incorrectly filtered them out.

Also includes: streaming+tools unification, system prompt improvements,
skills service cleanup, and defensive tool-call filtering.
Aligned system prompts with main branch's proactive tool usage guidance:

- DEFAULT_SYSTEM_PROMPT: 'Work iteratively until goals are fully achieved'
  instead of 'do not call tools unnecessarily'
- AGENT_MODE_ADDITIONS: 'Continue calling tools until the task is completely
  resolved' instead of 'Decide first: respond directly (no tools)'
- Added 'Prefer tools over asking users' and 'use them proactively'
- Tier-3 fallback prompt also updated to match proactive behavior
- Restored detailed file/command execution guidance from main
…stroying

On macOS, closing the main window (Cmd+W / red button) now hides it
instead of destroying it. This keeps the app visible in the Cmd+Tab
switcher and dock. The window is re-shown via:
- Clicking the dock icon (app 'activate' event)
- Keyboard shortcut (Ctrl+Shift+S)

The panel window has skipTaskbar:true and focusable:false, so when the
main window was destroyed, the app disappeared from Cmd+Tab entirely.

Implementation:
- Added isAppQuitting flag + setAppQuitting() export in window.ts
- Main window 'close' event on macOS: preventDefault + hide (unless quitting)
- before-quit handler calls setAppQuitting() to allow actual close
- activate handler shows existing hidden window instead of creating new one
1. Delegation priority: rules now appear BEFORE agent list with stronger
   wording — 'ALWAYS delegate BEFORE responding or asking for clarification'
2. Completion signal: numbered steps enforce respond_to_user BEFORE
   mark_work_complete; never put final answer in plain assistant text
3. Duplicate skills fix: skip loading profileSkillsInstructions when
   agentSkillsInstructions already present in profile snapshot
4. Self-delegation fix: added excludeAgentId param to constructSystemPrompt
   and getAgentsPromptAddition so sub-sessions don't list themselves
@aj47

aj47 commented Feb 23, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #1108.

This branch grew substantially after #1107 was opened, so I opened a fresh PR with an updated description and review guidance:
#1108

Closing this PR to keep review focused on the new PR.

@aj47 aj47 closed this Feb 23, 2026
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