Skip to content

feat(desktop): worktree agent data sync, retired persona cleanup, mcp_command reconciliation#728

Merged
wpfleger96 merged 10 commits into
mainfrom
wpfleger/worktree-agent-sync-retired-personas
May 27, 2026
Merged

feat(desktop): worktree agent data sync, retired persona cleanup, mcp_command reconciliation#728
wpfleger96 merged 10 commits into
mainfrom
wpfleger/worktree-agent-sync-retired-personas

Conversation

@wpfleger96
Copy link
Copy Markdown
Collaborator

@wpfleger96 wpfleger96 commented May 22, 2026

Worktree dev builds launched with SPROUT_SHARE_IDENTITY=1 show empty agent panels because each worktree gets its own ~/Library/Application Support/<identifier>/ data directory that starts with empty defaults. Meanwhile, users who ran Sprout before the Solo/Kit/Scout transition still carry 6 retired personas (Builder, Orchestrator, Planner, Refactor, Researcher, Reviewer) that clutter their persona list. And agents created before #584 still have mcp_command = "sprout-mcp-server" stored, causing goose to register the full 49-tool messaging MCP instead of using the CLI.

This PR adds three on-launch data management features plus a frontend fix:

Worktree agent-data sync via symlinks

sync_shared_agent_data() in migration.rs creates symlinks for managed-agents.json, personas.json, and teams.json from the current worktree's data directory to the canonical xyz.block.sprout.app.dev data directory. All dev instances share the same physical files — edits in any worktree (persona deletions, agent config changes, team updates) are immediately visible to all others, matching the mental model that "dev Sprout is one logical instance."

Guards: SPROUT_SHARE_IDENTITY=1, valid SPROUT_PRIVATE_KEY, canonical dir differs from current dir (via fs::canonicalize for case-insensitive APFS), canonical dir exists. Idempotent: correct symlinks are preserved, wrong/broken symlinks are replaced, regular files from prior launches are replaced with symlinks.

agents/packs/ is intentionally excluded — recursive directory symlink is out of scope. Pack personas appear in the worktree but ACP processes that read pack files at runtime may fail; install packs in the worktree separately if needed.

Atomic, symlink-preserving writes

All three agent data save functions (save_managed_agents, save_personas, save_teams) now use a shared atomic_write_json helper that resolves symlinks via canonicalize before doing the tmp-file + rename. Previously, managed-agents.json used an atomic write pattern that would destroy symlinks (rename replaces the symlink entry), while personas.json and teams.json used non-atomic fs::write. Now all three are consistent: atomic writes that preserve symlinks.

Retired persona migration

migrate_retired_personas() in personas.rs appends " (retired)" to retired personas' display names, sets is_active = false, and refreshes updated_at. Called from merge_personas() after the existing demotion pass.

Always soft-deprecates, never deletes. Removing records would orphan persona_id references in managed-agents.json and teams.jsonresolve_persona_env fails closed on missing personas, blocking agent spawn. Preserves the user's display name (a persona renamed to "My Reviewer" becomes "My Reviewer (retired)"). Idempotent: already-retired records (suffix present AND is_active = false) are skipped on subsequent launches.

The frontend's isPersonaActive previously returned true for all non-builtins regardless of isActive, so retired personas still appeared in the agents panel. Fixed by checking persona.isActive directly and passing filtered libraryPersonas to UnifiedAgentsSection instead of raw query data.

mcp_command provider reconciliation

reconcile_provider_mcp_commands() in migration.rs reads managed-agents.json on every launch, looks up each agent_command in the discovery table via known_acp_provider(), and patches mcp_command to the provider's canonical value. Goose/claude/codex get "" (no MCP server), sprout-agent gets "sprout-dev-mcp", unknown/custom agents are left untouched. This closes the gap left by #584 which only fixed creation-time defaults — existing agents with stale mcp_command = "sprout-mcp-server" now get corrected automatically.

Other changes

  • Extracted patch_json_records() helper used by reconcile_mcp_commands_in_file — eliminates duplicated read→parse→mutate→write boilerplate, includes parse-failure logging
  • Simplified RETIRED_PERSONAS constant from (&str, &str, &str) to (&str, &str) by dropping the unused display name field
  • Removed dead legacy migration code (migrate_legacy_data_dir, migrate_file, LEGACY_FILES) after upstream deletion in ab71ade9
  • Wired data management calls into lib.rs setup hook: sync_shared_agent_datareconcile_provider_mcp_commands → ... → restore_managed_agents_on_launch

@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 1f346d8 to 22f245a Compare May 22, 2026 21:29
@wpfleger96 wpfleger96 changed the title feat(desktop): worktree agent data sync + retired persona cleanup feat(desktop): worktree agent data sync, retired persona cleanup, and generalized skill symlinks May 22, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch 2 times, most recently from 55e79bd to 737773a Compare May 23, 2026 00:48
@wpfleger96 wpfleger96 changed the title feat(desktop): worktree agent data sync, retired persona cleanup, and generalized skill symlinks feat(desktop): worktree agent data sync + retired persona cleanup May 23, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 737773a to 4481cd9 Compare May 23, 2026 00:54
@wpfleger96 wpfleger96 marked this pull request as ready for review May 23, 2026 17:15
@wpfleger96 wpfleger96 requested a review from a team as a code owner May 23, 2026 17:15
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 4481cd9 to da60cbe Compare May 26, 2026 22:01
@wpfleger96 wpfleger96 changed the title feat(desktop): worktree agent data sync + retired persona cleanup feat(desktop): worktree agent data sync, retired persona cleanup, skill symlinks, mcp_command reconciliation May 26, 2026
@wpfleger96 wpfleger96 changed the title feat(desktop): worktree agent data sync, retired persona cleanup, skill symlinks, mcp_command reconciliation feat(desktop): worktree agent data sync, retired persona cleanup, mcp_command reconciliation May 26, 2026
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch 2 times, most recently from 504604a to 1403051 Compare May 26, 2026 22:17
wpfleger96 added a commit that referenced this pull request May 26, 2026
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
wpfleger96 added a commit that referenced this pull request May 26, 2026
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 372266b to 789e51e Compare May 26, 2026 23:23
wpfleger96 added a commit that referenced this pull request May 27, 2026
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from f852c06 to 64ce7c2 Compare May 27, 2026 00:39
wpfleger96 added a commit that referenced this pull request May 27, 2026
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 64ce7c2 to 055fb80 Compare May 27, 2026 16:27
wpfleger96 added a commit that referenced this pull request May 27, 2026
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from 1d4ac27 to e4acb85 Compare May 27, 2026 19:59
Worktree dev builds (SPROUT_SHARE_IDENTITY=1) had empty agent panels
because each worktree gets its own data directory that was never seeded.
Copy-with-overwrite on every launch syncs the 3 agent JSON files from
the canonical dev data dir, so worktrees see the same agents/personas/
teams as the main instance.

Also cleans up 6 retired personas (Orchestrator, Researcher, Planner,
Builder, Refactor, Reviewer) that persist from before the Solo/Kit/Scout
transition -- unmodified ones are removed, user-customized ones are
soft-deprecated as "<Name> (retired)" with is_active=false.
Both files grew past the default 500/900-line limits with the worktree
sync and retired persona migration additions.
…sona migration

The worktree agent-data sync copied managed-agents.json with runtime_pid
values intact, causing the worktree instance to kill the canonical
instance's running agents. The retired persona migration removed
unmodified records, orphaning persona_id references in managed-agents.json
and teams.json. The same-dir guard used byte comparison, which fails on
case-insensitive APFS.

Key changes:
- Scrub runtime_pid and 5 other volatile fields from copied
  managed-agents.json after sync (new scrub_managed_agents_runtime_state)
- Always soft-deprecate retired personas (never delete), preserving the
  user's display name and refreshing updated_at
- Use fs::canonicalize for the same-dir guard to handle symlinks and
  case-insensitive FS
- Extract sibling_data_dir helper to DRY legacy_data_dir and
  canonical_dev_data_dir
- Expand module docstring, SHARED_AGENT_FILES doc, and lib.rs ordering
  comment
- Replace tautology test, expand idempotency test to 4 cases, fix clippy
  &mut Vec → &mut [_] lint
cargo fmt expanded assert! blocks from single lines to multi-line,
pushing the file past the 620-line limit.
Existing agents created before #584 have mcp_command = "sprout-mcp-server"
stored in managed-agents.json. At spawn, this registers the full 49-tool
messaging MCP server — goose prefers live tools over CLI instructions.

On every launch, reconcile_provider_mcp_commands() reads the JSON, looks
up each agent_command in the discovery table, and patches mcp_command to
the canonical value (empty string for goose/claude/codex, "sprout-dev-mcp"
for sprout-agent). Unknown/custom agents are left untouched.

Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Extracts shared `patch_json_records` helper to eliminate duplicated
read→parse→mutate→write boilerplate between scrub and reconcile
functions. Fixes retired persona `is_active` guard to handle edge case
where display name already ends with " (retired)" but persona is still
active. Simplifies `RETIRED_PERSONAS` tuple by dropping unused name
field. Adds 3 new reconcile tests (absent key, null value, codex
provider).
…tion

Main (`ab71ade9`) removed the `migrate_legacy_data_dir` call and `mod
migration` declaration. Rebase kept `mod migration` (needed for worktree
sync + reconciliation) but left the legacy migration functions and their
8 tests as dead code. This commit removes them and drops the file-size
override from 870 to 610.
`isPersonaActive` returned true for all non-builtins regardless of
`isActive`, so retired personas (is_active=false, is_builtin=false)
still appeared in the agents panel. Fix the predicate to check
`isActive` directly, and pass `libraryPersonas` (already filtered)
to `UnifiedAgentsSection` instead of raw query data.
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from e4acb85 to ec38882 Compare May 27, 2026 20:26
The one-directional copy-on-launch approach silently overwrote edits
made in worktree instances (deletions, persona changes) because the
canonical dir always won. Symlinks make all dev instances share the
same physical files — edits in any worktree are immediately visible
to all others, matching the "one logical dev instance" mental model.

Also aligns all three agent data save functions (managed-agents,
personas, teams) on the same atomic, symlink-preserving write pattern:
canonicalize → tmp + rename at the resolved target.
@wpfleger96 wpfleger96 force-pushed the wpfleger/worktree-agent-sync-retired-personas branch from ec38882 to 9d1a7b5 Compare May 27, 2026 20:35
@wpfleger96 wpfleger96 merged commit f42839c into main May 27, 2026
15 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/worktree-agent-sync-retired-personas branch May 27, 2026 20:46
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