fix(state): persist model assignments across sync runs#265
Conversation
Model assignments selected via the TUI or install were only held in memory during the session. Every subsequent `gentle-ai sync` (TUI or CLI) rebuilt the Selection with empty assignment maps, causing sdd.Inject to fall back to the hardcoded "balanced" preset and silently overwrite the user's choices. Root cause: InstallState only serialised `installed_agents` — the ClaudeModelAssignments and ModelAssignments fields were never written to ~/.gentle-ai/state.json. This commit: - Adds ClaudeModelAssignments and ModelAssignments fields to InstallState (backward-compatible via omitempty) - Changes state.Write to accept the full InstallState struct - Persists assignments during both install and TUI sync flows - Loads persisted assignments in RunSync (CLI) and tuiSync (TUI) when no explicit overrides are provided - Adds round-trip and backward-compat tests for the new fields Fixes: model assignments reset to "balanced" on every sync
…c runs Covers CLI sync load path, TUI load/persist round-trip, read-merge-write safety, backward compat with pre-feature state files, and multi-sync cycle.
Alan-TheGentleman
left a comment
There was a problem hiding this comment.
Review: fix(state): persist model assignments across sync runs
Verdict: APPROVE
Clean, well-tested fix. The read-merge-write pattern is correct, backward compatibility is covered by the omitempty tags and the legacy-state test, and the 7 new tests cover the important scenarios (load, no-clobber, merge, no-op, multi-sync cycle, backward compat).
A few non-blocking observations:
-
Duplicated helpers —
claudeAliasesToStrings/modelAssignmentsToStateappear identically in bothappandcli. Acknowledged in the PR description as intentional to avoid coupling. If a third caller appears, consider extracting to a shared internal package. -
Inline vs. extracted load logic — The TUI path extracts
loadPersistedAssignmentsas a clean helper, butsync.gohas the same logic inline. Minor consistency nit. -
TUI persists after sync, CLI does not —
tuiSynccallspersistAssignmentsafter execution;RunSyncdoes not. Currently harmless since nothing mutates assignments between load and execute, but the asymmetry is worth a comment for future maintainers. -
ModTime-based no-op test —
TestPersistAssignmentsNoOpWhenEmptycompares file timestamps. On coarse-grained filesystems this could theoretically be fragile. Content comparison would be more robust, but unlikely to flake in practice.
Overall: solid work, good test coverage, clear PR description. Ship it.
…test engram setup claude-code writes to settings.json with non-deterministic Go map iteration order. First vs second run produces byte-different but semantically identical JSON, causing md5sum comparison to false-fail. Add json_files_equal helper using python3 for key-order-agnostic comparison and use it in test_idempotent_full_claude.
62c486e
into
Gentleman-Programming:main
…g#265 state.Write API change - Adapt uninstall service to use state.InstallState instead of []string - Update app.go dispatch to match RunUninstall return signature - Replace incompatible uninstall tests with PR-compatible version
* fix(uninstall): harden managed cleanup and atomic writes * fix(uninstall): address second audit findings * docs(usage): document granular uninstall flow * feat(tui): add uninstall mode selector with workspace asset warnings - Add ScreenUninstallMode as entry point for uninstall flow - Three modes: Partial (granular selection), Full (all agents/components), Full & Remove (includes binary deletion) - Workspace asset warnings for .engram/, workflows/, and skills/ - Full & Remove mode uses os.Remove() to delete gentle-ai binary after successful uninstall - Updated tests to reflect new navigation flow through mode selector - Closes part of #102 * feat(uninstall): report manual cleanup required items - Extend uninstall result with ManualActions - Report directories left non-empty after managed cleanup - Show manual cleanup section in CLI and TUI uninstall results - Add tests for service, CLI report, and TUI result rendering * fix(uninstall): resolve Copilot review feedback on TUI and service logic * feat(uninstall): add manual CLI command cleanup instruction on full uninstall * feat(uninstall): add 'Full Uninstall + Clean Install' mode Adds a 4th uninstall mode that removes all managed configuration and immediately re-syncs all managed assets from scratch. This provides a factory-reset workflow to fix broken configurations without manually re-running install/sync commands. The sync runs after uninstall completes. Sync errors are non-fatal and displayed separately in the result screen so the user always sees the uninstall status. * fix(uninstall): clean SDD profiles during removal * fix(uninstall): wire TUI uninstall path and onboard cleanup Ensure managed uninstall is reachable from the Welcome menu and correctly wired to the backend callback, and include sdd-onboard in SDD cleanup lists to avoid leftover managed artifacts. * fix(uninstall): resolve review regressions in full-remove and atomic writes Restore WriteFileAtomic permission-relaxation compatibility, prevent Homebrew full-remove from deleting managed binaries, and tighten uninstall cleanup safety checks so reviewer-reported edge cases are handled explicitly. * feat(uninstall): add profile sub-selection inside uninstall flow Add a conditional profile-selection step so users can choose which OpenCode SDD profiles are removed, while preserving the old flow when no profiles apply. * feat(uninstall): add Engram project-vs-global cleanup scope * fix: resolve rebase conflicts with main after PR #265 state.Write API change - Adapt uninstall service to use state.InstallState instead of []string - Update app.go dispatch to match RunUninstall return signature - Replace incompatible uninstall tests with PR-compatible version --------- Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
Closes #264
Summary
syncrunstate.jsonwhen no flags/overrides are providedInstalledAgentsand other state fields are preservedDesign note
claudeAliasesToStringsandmodelAssignmentsToStateare intentionally duplicated as unexported helpers inappandcli— avoids couplingstatetomodelfor two 8-line functions.Test plan
TestRunSyncLoadsPersistedModelAssignments— CLI sync loads from state.jsonTestRunSyncDoesNotOverridePersistedAssignmentsOnSecondSync— assignments survive sync1 → sync2TestRunSyncWithNoPersistedAssignmentsDoesNotPanic— backward compat with old stateTestLoadPersistedAssignmentsPopulatesEmptySelection— TUI load pathTestLoadPersistedAssignmentsDoesNotOverrideExisting— TUI overrides not clobberedTestPersistAssignmentsPreservesInstalledAgents— read-merge-write safetyTestPersistAssignmentsNoOpWhenEmpty— no spurious writesgo test ./...— 37 packages, 0 failures