feat(desktop): worktree agent data sync, retired persona cleanup, mcp_command reconciliation#728
Merged
Merged
Conversation
1f346d8 to
22f245a
Compare
55e79bd to
737773a
Compare
737773a to
4481cd9
Compare
4481cd9 to
da60cbe
Compare
504604a to
1403051
Compare
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).
372266b to
789e51e
Compare
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).
f852c06 to
64ce7c2
Compare
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).
64ce7c2 to
055fb80
Compare
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).
1d4ac27 to
e4acb85
Compare
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.
e4acb85 to
ec38882
Compare
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.
ec38882 to
9d1a7b5
Compare
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.
Worktree dev builds launched with
SPROUT_SHARE_IDENTITY=1show 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 havemcp_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()inmigration.rscreates symlinks formanaged-agents.json,personas.json, andteams.jsonfrom the current worktree's data directory to the canonicalxyz.block.sprout.app.devdata 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, validSPROUT_PRIVATE_KEY, canonical dir differs from current dir (viafs::canonicalizefor 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 sharedatomic_write_jsonhelper that resolves symlinks viacanonicalizebefore doing the tmp-file + rename. Previously,managed-agents.jsonused an atomic write pattern that would destroy symlinks (rename replaces the symlink entry), whilepersonas.jsonandteams.jsonused non-atomicfs::write. Now all three are consistent: atomic writes that preserve symlinks.Retired persona migration
migrate_retired_personas()inpersonas.rsappends" (retired)"to retired personas' display names, setsis_active = false, and refreshesupdated_at. Called frommerge_personas()after the existing demotion pass.Always soft-deprecates, never deletes. Removing records would orphan
persona_idreferences inmanaged-agents.jsonandteams.json—resolve_persona_envfails 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 ANDis_active = false) are skipped on subsequent launches.The frontend's
isPersonaActivepreviously returnedtruefor all non-builtins regardless ofisActive, so retired personas still appeared in the agents panel. Fixed by checkingpersona.isActivedirectly and passing filteredlibraryPersonastoUnifiedAgentsSectioninstead of raw query data.mcp_commandprovider reconciliationreconcile_provider_mcp_commands()inmigration.rsreadsmanaged-agents.jsonon every launch, looks up eachagent_commandin the discovery table viaknown_acp_provider(), and patchesmcp_commandto 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 stalemcp_command = "sprout-mcp-server"now get corrected automatically.Other changes
patch_json_records()helper used byreconcile_mcp_commands_in_file— eliminates duplicated read→parse→mutate→write boilerplate, includes parse-failure loggingRETIRED_PERSONASconstant from(&str, &str, &str)to(&str, &str)by dropping the unused display name fieldmigrate_legacy_data_dir,migrate_file,LEGACY_FILES) after upstream deletion inab71ade9lib.rssetup hook:sync_shared_agent_data→reconcile_provider_mcp_commands→ ... →restore_managed_agents_on_launch