fix(dry-run): stop dry-run and read-only commands persisting config migrations (#893) - #901
Merged
Merged
Conversation
…tion (Tencent#893) mcp inject loaded its scope bare, so --dry-run still saved a pending role migration, partition rename or self-mode bootstrap. It now forwards dryRun; mcp list is read-only and loads with dryRun: true unconditionally, as status and list do since Tencent#866.
…pdate (Tencent#893) roles list is read-only and loads with dryRun: true unconditionally.
…ove (Tencent#893) projects list and projects members are read-only and load with dryRun: true.
…t#893) tags list is read-only and loads with dryRun: true.
…ttp (Tencent#893) source list and source browse are read-only and load with dryRun: true. source list also reached a second bare load through loadLocalAgentConfig, whose HTTP backfill reads only repo.kind and repo.url; it now loads with dryRun: true, and the migration persists on the next command that writes.
…claude/repo (Tencent#893) --from-repo-list and --from-org reach the same load through importFromRepo.
…tus load read-only (Tencent#893)
…ist loads read-only (Tencent#893)
…encent#893) hooks list, members, exclude list, recall status and doctor load with dryRun: true unconditionally, as status and list do since Tencent#866.
…igrates nothing (Tencent#893) LOAD_ONLY_COMMANDS gains the read-only commands and the previews that stay clean on the legacy-role fixture. PREVIEWS covers the commands that fail past the loader on that fixture: it asserts only config.yaml, with the same call without --dry-run as the positive control that the load is on the path.
…t --from-claude (Tencent#893) scanCandidates resolves Claude's tool root before the loader import.ts already fixed, through a bare load in resolveMemberToolRoots, so the dry run still saved the migration. resolveConfigForDir and findUnreadableProjectConfig take the same optional LoadOptions for the read-only callers below; hook and usage callers pass nothing and behave as before.
…g it (Tencent#893) Forcing dryRun there made real hook runs print the [dry-run] migration preview and stop persisting it. source list now passes it through describeLocalAgent; every other caller is unchanged. source add-http forwards { dryRun: options.dryRun } like the rest of the file.
…ad-only (Tencent#893) detectTeam takes optional LoadOptions; the Stop-hook share gate passes none.
…ng changed sites (Tencent#893) Every dry-run row now asserts the loader logged its migration preview, so an early return cannot pass. Adds source browse, codebase --lint, skill list/show, webhook list, digest, projects update/remove, import --from-claude, and a config-only table for members, projects members and stats.
…n on a dry run (Tencent#893) The preAction hook passes dryRun to maybeMigrate, but planMigration and queueKeptInCheckout resolved the partition bare, so pull/push --dry-run still renamed a pre-Tencent#546 partition. Both now take the option.
…encent#893) blockReason fell back to shareGate(), a bare load, when no team was passed; only skill get, skill path and the catalog reach that fallback. The Stop hook still asks shareGate directly and is unchanged.
…ng its branch (Tencent#893) Also note in loadLocalAgentConfig that dryRun covers only its config.yaml load.
stats also loads bare per session through the dashboard scope helpers on the pull/report path; a partial fix would claim more than it does. Tracked in the follow-up issue with the other dry-run gaps.
…in blockReason (Tencent#893) Also correct the test comment on when the pre-command migration runs.
This was referenced Sep 29, 2026
|
…r dryRun (Tencent#893) The option reached only the config.yaml load. The legacy group-binding cleanup, the binding-key canonicalization and the HTTP backfill still saved config.json, so `teamai source list` could rewrite or create it. Under dryRun they now stay in memory, and the cleanup prints a `[dry-run] Would remove` preview instead of `Removed`.
|
Findings
Review Notes
|
…nt#893) models list loaded the scope read-only but read the team keys through loadTeamValues without the option, so a key a 0.26.0 beta stored under the profile id alone was bound to its gateway and the values file rewritten. It now binds the key in memory only; the next write command saves it.
|
No findings.
|
jeff-r2026
approved these changes
Sep 29, 2026
This was referenced Sep 29, 2026
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
--dry-runstill persisted config migrations (a legacy role migration, a pre-#546 partition rename, a self-mode bootstrap) because many commands loaded their scope bare. #866 fixed the loaders ofpull,push,statusandlist. This PR does the same for every other command that honours--dry-run(it forwards{ dryRun }), and for read-only commands, which now load withdryRun: trueunconditionally, the waystatusandlistdo.The same change, per call site:
{ dryRun }mcp inject,roles init/add/remove/update,projects add/update/remove,tags add/remove,source add/remove/add-http,remove,uninstall,packages install,import --from-iwiki/--from-mr/--from-claude/--from-repo(and--from-repo-list/--from-orgthrough it),codebase --reconcile/--deep-enrich,models switch, the CLI's pre-command auto-migration (planMigration,queueKeptInCheckout)dryRun: true(read-only)mcp list,roles list,projects list/members,tags list,source list/browse,hooks list,members,exclude list,recall status,doctor,models list,codebase --status/--lint,skill list/show/get/path,webhook list,digestShared helpers gained an optional
LoadOptionsthat defaults to{}:resolveMemberToolRoots,findUnreadableProjectConfig,detectTeam,loadLocalAgentConfig/describeLocalAgent,loadWebhookConfig,planMigration,queueKeptInCheckout. A caller that passes nothing (hooks, the Stop-hook share gate, usage tracking, the dashboard) behaves exactly as before. UnderdryRun,loadLocalAgentConfigalso keeps its own~/.teamai/local-agent/config.jsonmigrations (legacy group-binding cleanup, binding-key canonicalization, the HTTP backfill) in memory instead of saving them.Type of Change
Test Plan
npx tsc --noEmitpassesnpm run lintpassesnpx vitest runpasses (340 files, 5428 passed, 1 skipped)Unit:
src/__tests__/dry-run-load-path.test.ts(the #850/#866 file), on the fixture #866 uses: a userconfig.yamldue for the legacy role migration.Unit:
src/__tests__/local-agent.test.ts:loadLocalAgentConfig({ dryRun: true })on a legacy group binding, on two path aliases, and on an HTTPconfig.yamlwith noconfig.json. Each returns the migrated view and leavesconfig.jsonbyte-identical (or absent); the backfill case also checks that the same load without the flag creates it. All three fail at c19a8b3 and pass with the fix.Unit:
src/__tests__/pull-model-namespaces.test.ts:models list team:gwon a key a 0.26.0 beta stored under the profile id alone reports it (API key: environment COMPANY_KEY) and leaves the team values file byte-identical. It fails at 3da2705 (the file gainsteam:gw@https://gw.company.test) and passes with the fix. The existing tests in that file that bind such a key throughpullandmodels switchare the positive control.origin/main): every new row fails (config.yamlgainsprimaryRole: hai, or the legacy partition is renamed). Every positive control passes.End-to-end, real CLI (
npm run build, thennode dist/index.jsagainst a tempHOMEholding that same legacy config;mainisorigin/main671f509,branchis c19a8b3):(The hashes differ between the two runs only because the temp path is written into
repo.localPath.)teamai source listagainst a tempHOMEwhoselocal-agent/config.jsonholds a legacy group binding and two aliases of one workspace (beforeis c19a8b3,afteris this branch):teamai models list team:gwagainst a tempHOMEwhose team values file holds a beta keyteam:gw(beforeis 3da2705,afteris this branch):A second real-CLI probe, on a project whose partition still has its pre-#546 name:
teamai pull --dry-run,push --dry-runand--dry-run contribute --filerenamed the partition onorigin/mainand left it in place on this branch.pullwithout the flag still renames it. Provider-independent: the load path does not touch a provider, so one run coversgit/gitlab/github. No agent-specific path is involved.src/__tests__/push-env.test.tsfails intermittently under full-suite load on this machine, onorigin/maintoo (a different subset of its tests each run). It passes on its own (18/18). It is unrelated to this change.Related Issues
Closes #893
Follow-up: #900 (everything listed under "Deliberately not in this PR" below).
Notes for Reviewers
Deliberately not in this PR
These are real
--dry-runbugs found while sweeping for #893. They are left out on purpose: each has a different cause or needs its own design, and including them would bury the loader fix. They are known and tracked, so please do not report them as missing from this diff.recall maintenance --dry-run/recall promote --dry-runrun for real (prune, drafts, promote + publish)src/index.ts:1264,:1297,:1321,:1345,:1381--dry-runtoprogram.opts(), and these two actions read onlycmdOpts.dryRun(alwaysundefined). Threading their loaders alone would be a no-op. Destructive, so it gets its own PR and test.remove(pull + state save before its guard),source add(clone),import --from-repo*(clone + lock),models switch(re-bound keys saved),roles/projectsedits (git pull)statsstill migrates on loadsrc/dashboard-scope.ts:50,:103;src/session-owners.ts:82,:357--dry-runentirely:projects set,hooks inject/remove,mcp remove,exclude add/remove,recall enable/disable,update,review --apply,codebase --extract,init,init --httpenv list/add/remove,env execsrc/env-commands.tsLOAD_ONLY_COMMANDS. Whichever merges second rebases the table (a trivial conflict).import --from-repohas no test rowsrc/import-repo.ts:290dryRunis destructured three lines above it.Review record
Two-axis
/code-review(standards and spec) ran three times againstorigin/main. Pass 1 foundimport --from-claudestill loading bare throughresolveMemberToolRoots, read-only commands missed by the first sweep (skill list/show,webhook list,digest), and aloadLocalAgentConfigchange that would have printed the[dry-run]preview on real hook runs. Pass 2 found the pre-command auto-migration still renaming a partition onpull/push --dry-run(confirmed with the real CLI),skill get/path share, and thatstatscould only be fixed partially. All of these are fixed, or moved to the table above. Pass 3 found no spec gaps (it re-ranpull/push --dry-runandskill get shareon the built CLI, and each new row fails with its fix reverted) and two minor standards points, both applied. The Codex review then found thatloadLocalAgentConfigstill savedconfig.jsonunderdryRun, sosource listcould rewrite or create it. Fixed in 3da2705. A second Codex pass found thatmodels listread the team keys throughloadTeamValueswithout the option, so a beta key was re-bound and saved. Fixed in 277b4ae.Merge danger
Door: two-way. Each commit forwards an option or adds a defaulted parameter, and reverting one restores the old behaviour exactly.
Blast radius: config loading for the listed commands.
[dry-run] Would migrate legacy teamai config ...(on stderr underskill) instead of silently saving it. The migration persists on the next command that writes, as withstatusandlistsince fix(dry-run): thread { dryRun } through the loaders pull, push, status and list use #866.mcp list,doctorand the like now waits for a writing command. Nothing reads the migrated file in between, because every loader returns the migrated view in memory.source liston alocal-agent/config.jsondue for binding cleanup prints[dry-run] Would remove ...and leaves the file alone; the next hook run or bind saves it, as before.models liston a key a 0.26.0 beta stored binds it to its gateway in memory only; the nextmodels switch,models configureorpullsaves it, as before.