feat(migration): auto-migrate configs after CLI upgrade#25
Merged
josephgoksu merged 2 commits intomainfrom Mar 9, 2026
Merged
Conversation
After `brew upgrade`, users have stale configs (old tw-* slash command files and legacy taskwing-mcp MCP entries). This adds a lightweight version check to PersistentPreRunE that detects version mismatches and silently regenerates local configs on the next command. - Add internal/migration package with CheckAndMigrate() - Hook into root command PersistentPreRunE (skips version/help/mcp) - Stamp .taskwing/version during bootstrap for change detection - Silently regenerate slash commands for managed AIs on version change - Warn (stderr) about legacy global MCP server names - Sub-millisecond happy path (1 stat + 1 read + string compare) - 7 tests covering skip cases, migration, idempotency, and warnings
- Rename `init` variable to `initializer` (reserved identifier) - Walk parent chain to skip entire mcp subtree, not just leaf command - Warn on stderr when version stamp write fails (prevents silent retry loop) - Warn on stderr when slash command regeneration fails (visibility)
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.
Summary
brew upgrade, users have stale configs (oldtw-*slash command files, legacytaskwing-mcpMCP entries). This adds a lightweight version check toPersistentPreRunEthat auto-repairs local configs silently on the next command after upgrade..taskwing/versionduring bootstrap; on version mismatch, regenerates slash commands for all managed AIs (pruning staletw-*files) and warns about legacy global MCP server names.version,help,mcpcommands anddevbuilds.Files changed
internal/migration/upgrade.goCheckAndMigrate()core logicinternal/migration/upgrade_test.gocmd/root.gomaybeRunPostUpgradeMigrationintoPersistentPreRunEcmd/bootstrap.gointernal/bootstrap/initializer.goVersionfield,AIHelperByName()accessor, version stamp increateStructure()internal/bootstrap/service.goSetVersion()methodTest plan
go test ./... -count=1 -short— 193 tests pass across 32 packagesgo build ./...— clean buildecho "1.21.4" > .taskwing/version && ./bin/taskwing doctor→ verify oldtw-*files removed, newtaskwing/commands created, version updatedtaskwing mcp(stdio server) is not affectedGreptile Summary
This PR adds a lightweight post-upgrade migration system that detects CLI version changes after
brew upgradeand automatically regenerates stale AI slash command files and warns about legacy MCP server names. The design intent is solid — version-stamp at bootstrap, compare on every command invocation, migrate once and re-stamp. The happy path is correctly sub-millisecond and the migration is non-fatal throughout.Key issues found:
Broken
mcpsubcommand skip (cmd/root.go):cmd.Name()returns the leaf command name, so any subcommand undermcp(e.g.,mcp install) hascmd.Name()!="mcp", bypassing the guard. The migration will run for subcommands when it shouldn't. Fix requires walking up the parent chain to detect the entiremcpsubtree.Silent infinite re-migration loop (
internal/migration/upgrade.go): Both version stamp writes silently discard errors with_ =. On a read-only or full filesystem, the version is never updated, causing the migration to re-run on every command indefinitely with no user feedback — a hidden performance regression.initshadows Go built-in (internal/migration/upgrade.go): The local variableinit := bootstrap.NewInitializer(...)shadows Go's reservedinitidentifier.golangci-lintwill flag this; rename toinitializer.CreateSlashCommandserrors silently discarded (internal/migration/upgrade.go): Failed slash command regeneration (e.g., permissions) is swallowed — the broken state is marked as successfully migrated and never retried, with zero diagnostic feedback.The implementation approach is sound, but these four correctness gaps should be addressed: the
mcpskip guard doesn't protect subcommands, silent write failures create a performance regression, lint violations, and missing error visibility.Confidence Score: 3/5
mcpskip guard doesn't protect subcommands (allowing migration to run unexpectedly), both version write operations silently ignore errors (creating permanent re-migration loops on filesystem failures), theinitvariable shadows Go's built-in (lint failure), andCreateSlashCommandserrors are discarded (hiding broken states). The silent write-failure issue is particularly concerning because it turns the happy-path ~1ms check into a permanent per-command loop on any filesystem error. However, the migration is non-fatal and correctness gaps are fixable without redesign.internal/migration/upgrade.go(3 issues: init shadowing, write error handling, CreateSlashCommands errors) andcmd/root.go(mcp skip guard incomplete) require the most attention.Sequence Diagram
sequenceDiagram participant User participant RootCmd as cmd/root.go (PersistentPreRunE) participant Migration as migration.CheckAndMigrate participant FS as Filesystem (.taskwing/version) participant Bootstrap as bootstrap.Initializer participant MCPCfg as mcpcfg.IsLegacyServerName User->>RootCmd: taskwing <any-command> RootCmd->>RootCmd: initTelemetry() RootCmd->>RootCmd: maybeRunPostUpgradeMigration(cmd) Note over RootCmd: Skip if cmd.Name() == "version"|"help"|"mcp"<br/>(⚠️ does NOT match mcp subcommands) RootCmd->>Migration: CheckAndMigrate(cwd, version) Migration->>FS: os.Stat(.taskwing/) alt Not bootstrapped Migration-->>RootCmd: nil, nil (no-op) else Bootstrapped Migration->>FS: os.ReadFile(.taskwing/version) alt Version file missing Migration->>FS: os.WriteFile(version) ⚠️ error silently dropped Migration-->>RootCmd: nil, nil else Version matches Migration-->>RootCmd: nil, nil (happy path, ~sub-ms) else currentVersion == "dev" Migration-->>RootCmd: nil, nil (skip dev builds) else Version mismatch Migration->>Bootstrap: migrateLocalConfigs(projectDir) loop For each managed AI Bootstrap->>FS: Check marker file Bootstrap->>Bootstrap: NewInitializer(projectDir)<br/>⚠️ named "init" (shadows built-in) Bootstrap->>Bootstrap: CreateSlashCommands(aiName)<br/>⚠️ error silently dropped end Migration->>MCPCfg: checkGlobalMCPLegacy() MCPCfg-->>Migration: []warnings Migration->>FS: os.WriteFile(version) ⚠️ error silently dropped Migration-->>RootCmd: warnings, nil end end RootCmd->>User: fmt.Fprintf(stderr, warnings...)Last reviewed commit: d6a52c6