Skip to content

fix(init,uninstall): dry-run purity, --agent filter, stale-entry prune - #852

Open
fengjunhuii wants to merge 1 commit into
Tencent:mainfrom
fengjunhuii:worktree-fix-init-dryrun-agent-filter
Open

fengjunhuii wants to merge 1 commit into
Tencent:mainfrom
fengjunhuii:worktree-fix-init-dryrun-agent-filter

Conversation

@fengjunhuii

Copy link
Copy Markdown

fix(init,uninstall): dry-run purity, --agent filter, stale-entry prune

Three bugs found while setting up a single-repo (kind: self) workspace on Windows with init . --provider git --agent claude,codex. All three are fixed here, each with regression tests.

Bug 1 — init --dry-run wrote to disk

The preview created .teamai/, the tool directories, the hooks and the project partition config, and saved the local config. A preview could not be discarded: a follow-up real run stopped at already initialized unless --force was passed.

Fix: every write step in the single-repo init path (skeleton dirs, teamai.yaml, .gitignore, seed, hook injection, config save, state invalidation) is now gated on dryRun, and each prints what a real run would do instead:

[dry-run] Would create .teamai/teamai.yaml (mode: self)
[dry-run] Would seed tool dirs for: claude, codex
[dry-run] Would inject teamai hooks into: claude, codex
[dry-run] Would save local config to …/config.yaml

The teamai.yaml content the real run would write is emitted at debug level, and the seed/hook previews print the actual target lists (localConfig.enabledAgents / the hook filter), so a dry run is also a regression detector. The re-init guard is dry-run aware: it reports A real run would overwrite it (--force or confirmation) instead of prompting.

Bug 2 — --agent claude,codex still enabled other installed tools

Despite the explicit selection, a HOME-mirrored cursor leaked into enabledAgents and .cursor/ was seeded. Runtime tracing showed reconcileTeamHooksForConfig receiving the correct filter while enabledAgents was already polluted: initSelfRepo re-read the project config after Step 3 had written the mode: self marker. That read fires the clone-time self-heal bootstrap (#198), which detects every tool installed in $HOME and merges them into enabledAgents with no filter.

Fix: the pre-init project config is read once, before anything is written (hoisted above the marker write), and every later consumer (inheritUserScope, enabledAgents union, tool-roots carry-over, mode-switch settling) uses that snapshot. The bootstrap never sees a half-written install.

Bug 3 — uninstall --agent <tool> left a stale config entry

A tool whose files were already removed by hand reported Nothing to uninstall yet stayed in enabledAgents, so the next pull re-seeded its directories and its session-start hooks kept firing. (This is exactly how the cursor from bug 2 survived being "uninstalled" during setup.)

Fix: when a targeted removal plan is empty but the config still references the tool, the entry is dropped from enabledAgents and recorded in disabledAgents — the same durable exclusion a normal targeted uninstall writes. --dry-run previews it; no --force confirmation is needed since no file is touched. A tool the config does not reference at all still reports Nothing to uninstall.

Docs updated to match (docs/usage-guide.md + docs/usage-guide.zh-CN.md: the targeted-uninstall no-op paragraph now describes the stale-entry prune), plus CHANGELOG entries under Unreleased.

Test report

Unit tests (vitest run, Windows 10, Node 20):

Suite This branch origin/main baseline (e0bf2e9)
init.test.ts 5 failed / 50 passed 5 failed / 45 passed
uninstall.test.ts 4 failed / 52 passed 4 failed / 52 passed
Combined 9 failed / 102 passed (111) 9 failed / 97 passed (106)

All 9 failures on this branch are byte-identical (by test name) to the origin/main baseline on this machine — pre-existing Windows environment issues (EPERM on symlink creation, chmod/ENOENT), untouched by this PR. Net change: +5 passing tests, 0 new failures.

A full npx vitest run on this machine is unreliable as a signal: the vitest/tinypool worker pool crashes with ERR_IPC_CHANNEL_CLOSED during teardown (reproduced twice; 144 test files completed ✓ before the crash both times, zero test-level failures). Targeted per-suite runs are therefore the authoritative local signal above; CI on Linux runs the full suite.

  • Updated 3 tests whose old assertions encoded the buggy contract (dry-run saves config) to assert the new one (saveLocalConfigForScope not called + [dry-run] Would save local config preview).
  • New init regression tests: dry-run writes nothing — repo, tool dirs and config all stay untouched; reads the existing project config once, before the self-mode marker is written (asserts cursor absent from the seed preview).
  • New uninstall tests: stale-entry prune (enabled→disabled), dry-run preview without saving, and Nothing to uninstall for an unreferenced tool.

npx tsc --noEmit — clean. npm run build — clean.

End-to-end (real CLI, full offline matrix — 22/22 checks pass):

Isolated sandbox: fake HOME/USERPROFILE with pre-seeded .claude/.codex/.cursor, a bare git repo as the team remote, an scp-style remote (git@localhost:team/sandbox.git) driven by a GIT_SSH shim so git-upload-pack/git-receive-pack run against the local bare repo — no network. Sandbox rebuilt fresh per run (an earlier harness revision hit a fun trap: init auto-commits the skeleton, so a git checkout based reset resurrected the managed dirs and made every purity check fail — worth knowing when testing init locally).

  1. dry-run purity — init . --provider git --agent claude,codex --dry-run: repo .teamai/, .claude/, .codex/, .cursor/, the partition projects/ dir all absent, git status --porcelain empty, preview lines present.
  2. explicit --agent wins — real run: .claude/ + .codex/ created, .cursor/ not conjured, partition config enabledAgents: [claude, codex], no cursor anywhere in the log.
  3. stale-entry prune — cursor injected into enabledAgents by hand, uninstall --agent cursor --force: reports the stale prune, config ends with enabledAgents: [claude, codex], disabledAgents: [cursor].
  4. idempotent re-init — init --force again: .cursor/ still absent, enabled/disabled lists stable.

🤖 Generated with Claude Code

- init --dry-run wrote to disk: it created .teamai/, the tool dirs, the
  hooks and the project partition config, so a preview could not be
  discarded and a follow-up real run stopped at 'already initialized'
  without --force. Every write step is now gated on dryRun and previews
  what a real run would do instead.
- init --agent claude,codex still enabled other installed tools: the
  project config was re-read after the 'mode: self' marker was written,
  which fired the clone-time self-heal bootstrap (Tencent#198); it detected
  every tool in $HOME and merged them into enabledAgents ahead of the
  explicit selection. The pre-init config is now read once, before the
  marker exists.
- uninstall --agent <tool> reported 'Nothing to uninstall' for a tool
  whose files were already gone yet left it in enabledAgents, so pull
  kept re-seeding it. When the targeted plan is empty but the config
  still references the tool, the entry is dropped from enabledAgents
  and recorded in disabledAgents; --dry-run previews it, and a tool
  the config does not reference still reports nothing to uninstall.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@jeff-r2026
jeff-r2026 self-requested a review September 27, 2026 10:18
@jeff-r2026 jeff-r2026 self-assigned this Sep 27, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/init.ts:974 — loadLocalConfigForScope() is not read-only. On a freshly cloned repo already containing .teamai/teamai.yaml with mode: self but no local config, it invokes bootstrapSelfRepo(), which writes config/state, seeds tool directories, injects hooks, and registers the member. Thus init . --dry-run still writes, and init . --agent claude,codex can bootstrap HOME-detected tools such as Cursor before the explicit filter is applied.
  • [P1 blocking] src/uninstall.ts:1166 — enabledAgents: undefined means all installed tools are enabled, but this check treats the target as unreferenced. If a user manually removes a tool’s TeamAI files and runs uninstall --agent <tool> with the default implicit whitelist, no disabledAgents entry is saved, so a later pull recreates the resources. The empty-plan path should persist the exclusion unless the tool is already disabled.

Testing

  • The PR description includes a representative real-CLI end-to-end record, so its testing documentation is sufficient.

@jeff-r2026 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please resolve the P1 findings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants