fix(config): release only Claude's MCP servers on a root move; read the recorded root in import and skill tracking - #775
Merged
Conversation
…he recorded root in import and skill tracking
A re-init that moved the Claude Code root handed the full team config to
reconcileMcpForConfig({ removeAll }), which walks every MCP-capable tool,
so Codex, Cursor and the rest lost their teamai-managed servers until the
next pull. The release now narrows the team config to Claude.
import --from-claude scanned ~/.claude/rules and skill-use tracking only
knew the static ~/.claude/skills; both now resolve the recorded root. The
resolution (project config governing the directory, else user scope) moves
into resolveMemberToolRoots so the local agent, import and tracking agree;
tracking resolves it from the hook's reported directory. The helper checks
that the directory exists before probing, as resolveConfigForDir does, so a
hook from a deleted worktree still records — this also stops the local
agent from throwing on a missing workspace path.
Follow-up to Tencent#728 (third review pass, findings 1 and 5).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merged
8 tasks
The PR description includes a sufficient test plan and real-CLI end-to-end verification record. |
jeff-r2026
self-requested a review
September 24, 2026 07:01
jeff-r2026
approved these changes
Sep 24, 2026
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
…#404) Second review round on PR Tencent#780. Findings on config.ts / usage-tracker.ts remain false positives — this branch's diff does not touch either file (git blame attributes those lines to upstream Tencent#775). The rest are fixed: - Close the single-provider gate on the compat entry points too (review Tencent#5): `source add-http` and `init --http` now refuse to stand up the legacy singleton when a named HTTP provider exists, and `provider migrate-legacy` refuses when one already exists — so a named provider and a legacy singleton can never be dispatched together. - `provider sync` reports a failed remote command as a failure (review Tencent#5-cmd): processCommands now records a per-command failure into the SyncOutcome (still isolated — it neither throws nor aborts the loop), so the adapter returns ok:false instead of a false success. - Roll back injected hooks on a failed `provider add` (review #3): the rollback now runs the adapter's full teardown(), not just state + registry removal, so hooks init already wrote are not orphaned. - Make legacy migration genuinely resumable (review Tencent#4): a registry entry whose endpoint matches the legacy one is treated as this same interrupted migration and resumed (legacy stays authoritative until the marker); only a different-endpoint entry is a foreign conflict. - Reject Windows reserved names with an extension too — CON.txt, LPT1.foo (review Tencent#8) — and surface every invalid name as a clean error + exit, never an uncaught throw. Tests: reserved-name-with-extension, resumable migration after a registry write, migrate-legacy gate, and clean-exit on invalid name.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
) Third review round on PR Tencent#780. config.ts / usage-tracker.ts findings remain false positives (git blame attributes those lines to upstream Tencent#775; this branch's diff for both files is empty). The rest are fixed: - Restore migrate-legacy idempotency (review #3, a regression from the round-2 gate): the single-provider gate now rejects only a DIFFERENTLY-named provider, so re-running after a completed migration is a clean no-op and a crash-interrupted migration can still resume through the store's recovery path. - Fail `provider add` when init injected zero hooks (review Tencent#4): injectHooksToAllTools now returns an attempted/succeeded tally, and a named-provider init throws when hooks were attempted but none landed, so the add rolls back instead of reporting a false success. - teardown removes the built-in teamai hooks too (review Tencent#5): a named provider's removeLocalAgentHttp now clears the dispatch hooks its own init injected (via reconcileHooksToAllTools removeAll), gated on no other teamai install still needing them — so `provider remove` and failed-add rollback leave no orphaned hooks. - Serialize `provider add` under a machine lock (review Tencent#6): the single-provider check-and-write now holds ~/.teamai/providers/.add.lock so two concurrent adds cannot both pass the empty-registry gate. - Sync the bilingual management-backend design doc (review Tencent#7): it now points at providers/http/adapters/clawpro/client.ts and states the Tencent#404 phase 1-2 abstraction has landed. P2 (unused GitResourceProvider / registry class / adapter routes) is kept as the phase-1 abstraction contract by maintainer preference. Tests: migrate idempotency-after-success, init-injected-no-hooks rollback.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
…#404) Fourth review round on PR Tencent#780. config.ts / usage-tracker.ts findings stay false positives (empty diff vs origin/main; upstream Tencent#775 owns those lines). The rest are fixed: - Don't count an uninstalled adapter tool as a successful hook injection (review #3): injectHooksToAllTools now gates the pi/openclaw/hermes/ opencode/omp branches on the tool actually being installed, so a named-provider init that lands zero real hooks is detected and fails (was masked by adapter no-op "successes"). - teardown's built-in-hook removal now checks for ANY other teamai install — user scope AND every project-scope partition — not just the user config (review Tencent#4), so removing a named provider never strips hooks a project-only Git install still needs. - A re-written legacy singleton clears the stale migrated-to marker (review Tencent#5): initLocalAgentHttp (legacy path) calls clearLegacyMigrationMarker, so migrate → remove → source add-http / init --http reactivates the singleton instead of leaving it dormant. - teardown keeps the provider home + manifest when any resource uninstall fails (review Tencent#6), throwing so cleanup can be retried rather than orphaning resources with a destroyed ownership manifest. - Serialize migrate-legacy under the same machine lock as provider add via a shared withProviderLock helper (review P3). - source list no longer shows a migrated legacy snapshot as an active HTTP source (review P2). P2 (unused GitResourceProvider / registry class) kept as the phase-1 abstraction contract by maintainer preference. Tests: injection tally excludes uninstalled tools, marker reactivation.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
…nt#404) Sixth review round on PR Tencent#780. config.ts / usage-tracker.ts stay false positives (empty diff vs origin/main; upstream Tencent#775 owns those lines). The rest: - Redact the migration token in staging BEFORE publishing the provider home (review #3): the credential is written to its isolated 0600 file and stripped from the staged config.json, then the dir is atomically moved — so the published config.json never contains the token, even in a crash window. - teardownAllPlugins reports an aggregate success flag (review Tencent#4): it swallowed each plugin's uninstall error and returned void, so the caller's uninstallFailed never tripped. removeLocalAgentHttp now checks the return value and keeps state (plugins.json) on failure. - provider-add rollback keeps state when its teardown could not fully clean up (review Tencent#5): it drops only the registry entry (so dispatch won't load a broken provider) and preserves the home for a retriable `provider remove`, instead of destroying the ownership manifest. - `teamai uninstall` warns when a named provider's teardown was incomplete before deleting ~/.teamai (review Tencent#6), so orphaned external hooks/plugins are surfaced rather than silently stranded. - teardown's other-install check now also detects a legacy in-tree <project>/.teamai config governing the cwd (review Tencent#7 residual). - Fix the credential path in join-member.md — it is ~/.teamai/credentials/<name>, not the provider home (review P2 doc). P2 (source list showing a migrated snapshot) was already fixed in the prior round and is verified hidden; P2 (unused abstraction) and P3 (compat entries not under the provider lock) are as previously decided.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
…t#404) Seventh review round on PR Tencent#780. config.ts / usage-tracker.ts remain false positives (empty diff vs origin/main — upstream Tencent#775 owns those lines; asked maintainer to dismiss). The genuinely new findings — all flaws in the previous round's own fixes — are addressed: - Give the kept-on-failure state a real CLI retry path (review #3): a failed add that keeps its home now also writes provider.json there, and `provider remove` recovers a provider from its home config when the registry entry is gone, then keeps the home again if teardown still can't finish. The retry `provider remove <name>` the message promises now actually works. - `teamai uninstall` no longer deletes ~/.teamai when a provider teardown was incomplete (review Tencent#4): it keeps the dir (and its ownership manifests) and tells the user to resolve the issue and re-run, so the leftover external hooks/plugins stay recoverable instead of stranded. - Stop stripping the SHARED built-in dispatch hooks on a plain `provider remove` (reviews Tencent#5/Tencent#7): they serve every teamai install and no-op without config, so a leftover is harmless while an erroneous removal breaks a coexisting install — and another install can't be enumerated reliably (in-tree <project>/.teamai lives anywhere). Full built-in-hook removal now happens only under `teamai uninstall` (TEAMAI_UNINSTALL=1) when no other install remains. Tests: clean rollback vs. keep-on-teardown-failure, and provider remove recovering a registry-less home.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
…over (Tencent#404) Ninth review round on PR Tencent#780. - Run the legacy singleton fallback ONLY when no named provider is configured (review #3): once a named provider exists it owns delivery, so during the migration window (registry entry published, legacy dir not yet deleted) the hook dispatcher no longer runs both — which had caused duplicate report / command execution / resource writes. - Migration removes the shared legacy ~/.teamai/token when it was the credential source (review P2): the token now lives only in the isolated 0600 credential file, so the old plaintext copy is deleted instead of stranded after a later `provider remove`. Not changed: - Tencent#4 (provider remove leaving shared built-in dispatch hooks): kept per the maintainer's explicit decision — those hooks serve every install and no-op without config; full removal stays with `teamai uninstall`. - #1/#2 (config.ts / usage-tracker.ts): false positives, not in this PR's diff (upstream Tencent#775); asked the maintainer to dismiss. - P3 (compat entries not under the provider lock): they refuse when a named provider exists, so they cannot create a second active backend. Tests: legacy ~/.teamai/token removed on migration.
jeff-r2026
pushed a commit
to jeff-r2026/teamai-cli
that referenced
this pull request
Sep 24, 2026
Tenth review round on PR Tencent#780 (P2 cleanup). - Remove the `--priority` flag from `provider add http` and `provider migrate-legacy`, and the parsePriority helper. With a single provider it never affects behavior, so exposing and persisting it is the unused/speculative config the review rules warn against. The `priority` data-model field stays (defaulted to 50, reserved for the later arbitration phase) but is no longer user-configurable. Docs and the generated command reference updated to match. Not changed (as previously decided): #1/#2 config.ts/usage-tracker.ts are false positives outside this PR's diff (upstream Tencent#775); provider remove intentionally leaves the shared built-in dispatch hooks (maintainer decision); the compat entries refuse a second backend so they need no shared lock.
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
Two follow-ups to #728, from the review's third pass. A re-init that moves the Claude Code root released the teamai-managed MCP servers of every MCP-capable tool, not only Claude's:
reconcileMcpForConfig(…, { removeAll: true })walks every tool of the config it is handed, so Codex, Cursor and the rest lost their servers until the nextpullput them back. The release now hands it a team config narrowed to Claude. And two readers still hard-coded~/.claude:import --from-claudescanned~/.claude/rules, and skill-use tracking (skillExistsOnDisk) only knew the static~/.claude/skills, so a relocated Claude Code's rules were not importable and its skills were rejected as not installed. Both now read the recorded root: import through one helper,resolveMemberToolRoots(the resolution the local agent already used — project config governing the directory, else user scope — moved intoconfig.tsso the readers agree), and tracking through the same helper, given the hook's directory, so a skill under the root that governs the project the hook fired in counts.Type of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpasses (4500 tests)New/updated tests:
init.test.ts(the MCP release is handed a team config whosetoolPathshold Claude only),usage-tracking.test.ts(+2: a skill installed only under the giventoolRoots.claudecounts as present and not otherwise; a slash command from a hook whose directory's project config records a relocated root is tracked when the skill lives only there; a project config without a record follows the user-scope one; a hook that reports a directory that no longer exists — a deleted worktree — still records, under user scope),import-local-relocated-root.test.ts(new, 1:import --from-claudescans the recorded root'srulesand not~/.claude/rules).mcp-reconcile.test.ts(+1:removeAllon a config narrowed to Claude removes Claude's server only — Cursor's file and manifest rows stay — the runtime counterpart of theinitassertion).Mutation checks: handing the full team config to the MCP release fails 1 test; dropping the recorded root from
skillExistsOnDiskfails 1; not passing the resolved roots fromtrackSlashCommandfails 1; dropping the root from the import scan fails 1; probing a missing hook directory without the existence guard fails 1 (Cannot use simple-git on a directory that does not exist).Real-CLI end-to-end (built
dist/index.js, isolatedHOME, local bare repo bridged from a synthetic HTTPS URL, Claude and Codex both installed, a team MCP server inmcp/mcp.yaml, seedtoolPathsdeclaringmcpfor both tools):main(55b71ef):init→pulllands the server in~/.claude.jsonand~/.codex/config.toml(1 each). Re-initwithCLAUDE_CONFIG_DIR=~/.claude-aprintsRemoved 2 teamai-managed MCP server(s)and, before the nextpull, Codex's count is 0; thepullrestores it.initprintsRemoved 1, Codex stays at 1 throughout,~/.claude.jsongoes 1 → 0 and~/.claude-a/.claude.jsongets the server on the nextpull. Same on the second move (~/.claude-a→~/.claude-b) and on the blank-variable clear.Related Issues
Follow-up to #728 (third review pass, findings 1 and 5).
Notes for Reviewers
uninstallrelies on; the release is the one caller that means a single tool, so the narrowing belongs at the call site.resolveMemberToolRootskeeps the local agent's resolution order (project config governing the directory, else user scope) and only moves it intoconfig.ts; the local agent's behavior is unchanged. A project config without a record follows user scope on purpose: it is the ruleinitapplies when it fills a project config in, and a user-scopeinitmay record the root after the project was set up. Tracking calls it with the hook's directory (resolveHookCwd), so the hook's directory, not the process cwd, decides.~/.claudeskips the MCP move), only releases delivered model config in user scope, and resolves the old root fromdirname(settings). Each has a narrow trigger (an explicit default, HTTP mode in project scope, a customized nestedsettingspath); they are separate changes if wanted.memberToolRootsnow goes through the same helper, so a workspace path that no longer exists resolves to the user-scope record instead of throwing fromsimpleGit. Existing directories behave as before. A project config that is present but unreadable still falls back to user scope here (as the inline code did);resolveConfigForDirtreats that case as "no config" — left as is, noted for a later pass.resolveMemberToolRootschecks that the directory exists before probing for a project config, the wayresolveConfigForDiralready does:simpleGitthrows on a directory that is gone (a deleted worktree), and a slash command from such a hook must still be recorded.--from-claudeoption text insrc/index.tsnames the recorded root andskill-data/core/references/commands.mdis regenerated from it (npx vitest run commands-reference -u). Thesetupskill already describes the recorded root.🤖 Generated with Claude Code