fix(env): detect the right shell profile file on Windows (#682) - #693
Conversation
SHELL is never set on Windows, so detectShellProfile() always fell back to ~/.bashrc — but Git Bash starts as a login shell, which reads ~/.bash_profile, ~/.bash_login or ~/.profile, never ~/.bashrc. The env block was written correctly and looked correct on inspection, yet no shell ever sourced it. Give detectShellProfile a Windows branch that prefers an existing login-shell file, in that order, and falls back to .bashrc only when none exist — matching Git for Windows' own fallback in /etc/profile.d/bash_profile.sh, so both agree on what gets sourced. The logic previously lived twice (resources/env.ts and uninstall.ts); both now delegate to a single utils/shell-profile.ts so pull, doctor and uninstall can never resolve to different files. platform is an injectable parameter (default process.platform), same convention as resolveCliPath in utils/cli-path.ts, since CI only runs ubuntu/macos and a hardcoded process.platform would leave the win32 branch permanently uncovered — which is how this went unnoticed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Two P1s from the automated review on Tencent#693: - detectShellProfile ignored SHELL entirely on win32. A zsh installed via MSYS2/Cygwin sets SHELL just like it does on POSIX, while native Windows Node still reports platform === 'win32'; the Windows branch was taking over unconditionally and silently stopped loading .zshrc for that setup. SHELL is now checked before the platform branch, on every platform. - uninstall only looked at the one file detectShellProfile() resolves to today. The Tencent#682 fix changed which file `pull` prefers, so a machine last pulled with an older CLI can carry a stale block in a file the current resolution no longer points at, and a plain uninstall silently left it behind. buildRemovalPlan now scans every profile file teamai could ever have written to (.zshrc/.bashrc/.bash_profile/.bash_login/.profile, plus a configured override) and cleans every one that still carries a block. Verified both with a real CLI build against the exact scenarios described: zsh-on-Windows via SHELL, and a stale .bashrc block surviving alongside a freshly-written .profile block until `uninstall` now removes both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both P1s in 7c1659b:
Both verified with unit tests (new cases in |
The PR description includes unit/type-check results and a concrete real-CLI end-to-end record, so it does not lack a test plan or e2e notes. |
…(review) Two more findings from the automated review on Tencent#693: - buildRemovalPlan scanned every candidate profile filename for the generic TEAMAI_ENV_START marker alone, so uninstalling one scope (e.g. a project) could delete a completely different scope's still-active block just because it happened to live in one of the same candidate files. A candidate now only counts when its block's source line actually points at THIS scope's own env.sh (getDataHome(localConfig)/env.sh), reusing the same block-parsing logic doctor already relies on for the equivalent check. - extractEnvBlock/envBlockSourcesPath were private to doctor-delivery.ts; moved into utils/shell-profile.ts and imported by both doctor-delivery and uninstall so there is one implementation of "does this block belong to this data home", not two that could drift the same way detectShellProfile itself did in Tencent#682. - Documented the Windows shell-profile selection order and the scoped-cleanup behavior in usage-guide.md and its zh-CN counterpart. Verified with new unit tests (a stale-block-survives-in-another-scope case, and the original same-scope migration case using the real generated block format instead of a `~/env.sh` shorthand) and a real CLI build reproducing an unrelated scope's block surviving an uninstall that only touches its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both new P1s in 4043f69:
All three rounds of fixes re-verified together: unit tests pass ( |
|
… (review) envBlockSourcesPath compared the raw path against the block, but generateShellBlock always wraps the path in single quotes via shellQuoteValue, which escapes an embedded apostrophe as `'\''`. A path like /home/O'Brien/.teamai/env.sh never appears as a contiguous raw substring in the generated block, so doctor reported a correctly loading block as broken, and uninstall could not find it to clean up. Moved shellQuoteValue out of resources/env.ts into the shared utils/shell-profile.ts (env.ts now imports it) so the check compares against the exact string the generator would produce, instead of re-deriving the same escaping rule a second time. Added unit tests for envBlockSourcesPath covering the apostrophe case, a plain path, a non-matching path, and the pre-existing Tencent#661 unquoted Windows path case, using the real shellQuoteValue rather than hand-written escaping to avoid re-encoding the same assumption twice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed the P1 in a983ef0:
Note on verification: I tried to also reproduce this with a real CLI build against an actual |
No additional code defects found in the reviewed diff. |
|
On the compatibility-matrix request: I don't think it applies to this diff, and I'd rather explain why than pad the test plan with entries that don't add signal. The full changeset (
None of these reference The reason is structural: the shell-profile env block is a single OS/shell-dependent mechanism ( My existing real-CLI verification already exercises the one provider capability this PR's code path actually uses ( Happy to add specific matrix entries if a maintainer sees an interaction I'm missing — but I'd want to know what that interaction is rather than filling in results for combinations this change can't affect. |
Windows 11 real-machine validation of this PR (built from
|
| step | result |
|---|---|
before pull — bash -lc |
E2E_TOKEN = <unset> |
teamai pull --force |
Synced 1 env variable(s) → block written to ~/.profile |
after — bash -lc |
E2E_TOKEN = e2e-secret-value |
teamai doctor |
✔ Env variables injected in shell profile |
detectShellProfile() resolves to .profile on Windows exactly as intended, and the block now reaches the login shell that teamai's own hook commands run under. Confirmed on hardware, not by reading.
2. Finding — envBlockSourcesPath() matches one spelling only, and that hides the legacy block
posixPath is envShPath.split(path.sep).join('/'), i.e. D:/…/.teamai/env.sh, and the function requires either shellQuoteValue(posixPath) or that substring. A block written by a pre-#680 CLI carries backslashes, so neither matches — even though it names the same env.sh of the same scope. Two observable consequences in the run above:
doctorstays green while a dead block remains. Afterpull --forcewrote.profile,.bashrcstill held the unusable backslash block anddoctorreported no problem — it only inspects the filedetectShellProfile()resolves to.uninstalldoes not clean it.teamai uninstall --forceprinted✔ Cleaned shell profile: …\.profileand left the.bashrcblock exactly as it was. So the upgrade path named in the PR description — "a stale block in.bashrc…uninstallnow removes both" — holds only when the stale block happens to be in the drive form. For anyone upgrading from a pre-fix(env): make the shell-profile block load on Windows #680 build it does not, which is the common case now that fix(env): make the shell-profile block load on Windows #680 landed yesterday.
A milder variant of the same shape, seen on our own machine: the block there is functional but spelled in the MSYS drive form /c/… (written by a locally patched 0.24.0, not by a release). envBlockSourcesPath rejects it, and doctor prints "a POSIX shell reads an unquoted backslash as an escape" — a reason that is simply not true of a correctly quoted, working block.
Suggestion: make the comparison spelling-insensitive — normalize both sides first (drive letter ⇄ /c/, \ ⇄ /, quoted or not) instead of looking for one exact string. SHELL_PROFILE_CANDIDATE_NAMES already gives you the file set; the same normalization applied to the source target would let both doctor and uninstall see the legacy block.
3. Finding — the injection side has no counterpart to the new multi-file cleanup
buildRemovalPlan now scans every candidate profile, but injectShellProfile() still writes only the single file detectShellProfile() resolves to:
private async injectShellProfile(profilePath: string, block: string) { … }On this fixture pull --force therefore produced two teamai blocks — live in .profile, dead in .bashrc — and nothing migrates or removes the old one. Combined with (2) that is permanent: doctor does not look there, and uninstall does not recognise it. Even where the legacy block is recognised (drive form), the state is ambiguous, because that block's source may point at another scope's env.sh and inject wrong values into whatever shell reads that file. Since the scope check you added already makes the match precise, migrating (write the resolved profile, drop this scope's block from the legacy file) — or at minimum having doctor name the command that performs the migration — would close it.
4. Two smaller notes
~/.profileexisting is the reason Git for Windows'/etc/profile.d/bash_profile.shdoes not synthesize the~/.bash_profilethat would have sourced.bashrc; its guard is[ -e ~/.bashrc ] && ! -e ~/.bash_profile && ! -e ~/.bash_login && ! -e ~/.profile. Worth a sentence in the PR body, because it makes the bug look environment-specific when it is not. A fresh-$HOMEmatrix on this box confirms the order you implement:.bashrconly → Git for Windows creates a sourcing.bash_profile→ loads;.bashrc+.profile→.profileis read and.bashrcnever is;.bashrc+.bash_login→.bash_login.- Unrelated to this change, but seen in the same build:
teamai pullprintsSkipping hook injection for codebuddy, workbuddy: no shell is available in this environment. That is the/bin/shgate (windows codebuddy跳过hook #579/fix(hooks): run CodeBuddy's Windows hooks through cmd.exe #637), untouched here.
5. On the compatibility-matrix finding, and on #684
We agree with your reply at 06:30. Nothing in this diff branches on the git provider or on per-tool hook code; re-running the same profile logic under CodeBuddy/OpenCode or against a GitLab-hosted repo would only re-confirm code this PR does not modify. We would rather see the two findings above addressed than the matrix padded.
On #684: it does not touch src/uninstall.ts at all, so the bot's P1 there stands, and it has neither the shared helper nor the injectable platform that makes the win32 branch genuinely assertable on the Linux/macOS runners. We would suggest landing this PR and either closing #684 or reducing it to the win32 test cases it can still contribute.
We can re-run the transcript above against any later commit and post it verbatim if that is useful.
…(review) Two real gaps found by @CarlosWonMore's hardware validation on real Windows 11 machines, both stemming from envBlockSourcesPath only recognizing the current writing format: - A pre-Tencent#661 block (raw, unquoted, unconverted backslashes) or a locally-patched build's MSYS/Cygwin drive-form block (/d/Users/...) names the same scope's env.sh but never matched, so uninstall could not find it and doctor never flagged it — a dead block could survive every pull and every uninstall indefinitely. Added envBlockReferencesDataHome: a looser "does this block belong to this scope" check (current + legacy spellings, quoted or not) that answers ownership, kept separate from the existing envBlockSourcesPath which answers "does this block actually load" and must stay strict — conflating them would make doctor stop reporting a real Tencent#661-style break just because a legacy spelling happens to match. - uninstall.ts's candidate scan now uses envBlockReferencesDataHome, so a legacy block for this scope is found and removed regardless of which format wrote it. - doctor's env-delivery check now also scans the other candidate files for a stray block belonging to this scope and reports it, naming `teamai uninstall` as the fix — closing the "doctor stays green while a dead block remains" gap. Since this check also runs automatically after every `pull`, a stale legacy block now surfaces immediately instead of staying invisible. Deliberately not doing in this round: automatically migrating/deleting a legacy block during `pull` itself. `doctor` (and `pull`'s own post-pull check) now names the file and the fix, and `uninstall` performs it — that closes the visibility and cleanup gap without teaching `pull`'s write path to also delete files elsewhere, which is a larger behavioral change worth its own review. Verified with unit tests for envBlockReferencesDataHome (current form, pre-Tencent#661 raw form, MSYS drive form, different-scope negative case), a new uninstall test cleaning a pre-Tencent#661 legacy .bashrc block, a new doctor test flagging a stray legacy block, and a real CLI build reproducing the reviewer's exact repro end-to-end: pull writes the new block to .profile, doctor immediately flags the leftover .bashrc block and names `teamai uninstall`, and uninstall removes both. Also refined the Windows fallback description in usage-guide.md / usage-guide.zh-CN.md with Git for Windows' exact guard condition, per the reviewer's note that the bug looks environment-specific without it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@CarlosWonMore — thank you for the real hardware validation, this is exactly the kind of testing an emulated repro can't substitute for. Addressed both findings in aea31ef. Finding 2 (legacy spellings invisible to Finding 2's "doctor stays green" half, and Finding 3's "at minimum name the command" ask:
Finding 3 (migrate during Finding 4 (docs): updated both usage-guide docs with Git for Windows' exact guard condition ( Also added the unrelated MSYS drive-form case as its own unit test rather than only the pre-#661 raw form, since your report named both as things you'd actually seen. Thanks also for weighing in on the compatibility-matrix thread and on #684 — appreciated. |
|
…t test bug (review) Bot review on aea31ef found a real regression: with sharing.env.shellProfilePath: ~/.profile, profilePath stayed the literal unexpanded string while the stray-block scan's candidates are always absolute, so the resolved file never matched itself (candidate === profilePath) and got reported as a stray copy of its own valid block. Expand profilePath once, up front, mirroring the pattern uninstall.ts already used for the same override. Also: CI (ubuntu/macos) caught a real bug in the previous commit that my own Windows-host testing couldn't — candidateSpellings converted backslashes to forward slashes via `envShPath.split(path.sep).join('/')`, which is a no-op on a POSIX runner even for a Windows-shaped input path, since path.sep there is '/'. Moved isWindowsFormPath (shape-based, already used by generateShellBlock) out of resources/env.ts into the shared utils/shell-profile.ts and used it instead, so the conversion is correct regardless of which host runs it — matching the pattern the rest of this file already established for platform-independent logic. Fixed one test that fell into the same trap from the other direction: it fabricated a Windows-style block by replacing `/` with `\` in a POSIX-on-CI envShPath, producing a string with no drive letter at all that doesn't correspond to any real on-disk state. Rewrote it to use an unquoted (not backslashed) legacy form, which is realistic on any host; the Windows-specific legacy spellings are already covered directly in shell-profile.test.ts with explicit Windows-shaped literals. Added a regression test for the ~/.profile-override case and re-verified with a real CLI build: pull + doctor no longer report a tilde-configured profile as carrying a stray copy of its own block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed the P1 in cd739c4, plus a real CI failure the previous commit introduced. The reported bug ( Unplanned but important: CI (ubuntu/macos) failed on the previous push and caught something my own Windows-only local testing couldn't. CI is green again on this push; will keep an eye on it. |
|
A valid, working delivery reported as failed just because a stray
leftover block sits in another candidate file — buildEnvDeliveryCheck
folded "does this scope's env reach a shell" and "is there dead cruft
to clean up" into the same problems list, so a user whose delivery
genuinely works saw doctor/pull report failure until they ran
`teamai uninstall` or edited files by hand.
Split envDeliveryProblems to return { problems, staleProfiles }, and
buildEnvDeliveryCheck now returns two independent Checks: "Env
variables injected in shell profile" (unchanged delivery-correctness
semantics) and a new "No stale env blocks left behind" that only fails
when a legacy/duplicate block for this scope is found elsewhere,
naming `teamai uninstall`. A working delivery now reports healthy
regardless of leftover cruft; the cruft is still surfaced, just not as
the same failure.
Re-verified with a real CLI build reproducing the reviewer's exact
scenario: `doctor` now shows "Env variables injected in shell profile"
as a pass and "No stale env blocks left behind" as a separate,
distinct failure, instead of one conflated failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed the P1 in 5f762ac. Severity finding ( A working delivery now reports healthy; the leftover is still surfaced, just not as the same failure. I went with "split into two checks" over "auto-migrate during Compatibility matrix (repeated): this was already addressed in my reply at 06:30:41, and @CarlosWonMore independently agreed at 08:19:27 ("Nothing in this diff branches on the git provider or on per-tool hook code... We would rather see the two findings above addressed than the matrix padded"). Standing on that — happy to revisit if a specific interaction is named. P2: legacy-format matching is speculative scope creep: these aren't speculative. @CarlosWonMore reported both formats from real hardware in this same thread — the raw/unquoted pre-#661 form ("we reported #661/#672, filed #686 and #682... this is our daily platform") and the MSYS drive form ("seen on our own machine: the block there is functional but spelled in the MSYS drive form |
Windows 11 real-machine re-verification:
|
| # | check | aea31ef |
cd739c4 |
|---|---|---|---|
| 1 | legacy in-scope block (raw, unquoted, unconverted backslashes) left in .bashrc — does pull/doctor flag it? |
✅ flagged; names only .bashrc; names teamai uninstall |
✅ same |
| 2 | does uninstall --force clean both .profile and .bashrc? |
✅ both cleaned, 0 blocks left | ✅ same |
| 3 | same block in the MSYS drive form (/d/Users/...) |
✅ flagged and cleaned | ✅ same |
| 4 | sharing.env.shellProfilePath: ~/.profile |
❌ false positive — reported .bashrc, .profile |
✅ only .bashrc |
Row 3 is the shape our own machine actually carries. Our ~/.bashrc holds
[ -f "/c/Users/Carlos/.teamai/env.sh" ] && source "/c/Users/Carlos/.teamai/env.sh"
written by a locally-patched build. Before this PR both doctor and uninstall were blind to it — uninstall printed only Cleaned shell profile: …\.profile and the .bashrc block survived permanently. It is now found, flagged, and removed. That closes both findings I reported; thank you for turning them around.
Row 4 is the bot's P1 reproduced. With shellProfilePath: ~/.profile and .profile present, aea31ef flagged its own target file as a stray leftover:
✖ Env variables injected in shell profile
→ …\tilde\.bashrc, …\tilde\.profile still carries a teamai env block for
this scope from an earlier install; run `teamai uninstall` to remove it…
cd739c4 reports only the genuine stray:
✖ Env variables injected in shell profile
→ …\tilde\.bashrc still carries a teamai env block for this scope from an
earlier install; run `teamai uninstall` to remove it…
So expandHome() is confirmed on hardware, not merely by reading the diff. A label-normalized diff of the two full transcripts shows row 4 as the only behavioural difference — rows 1–3 are byte-identical, so nothing regressed in the paths I originally reported.
Also
- Your own new tests pass here too:
vitest run src/__tests__/shell-profile.test.ts src/__tests__/uninstall.test.ts→ 52/52 passed on Windows, including the threeenvBlockReferencesDataHomespellings and the pre-[bug] Windows: env 注入到 shell profile 时写的是原生路径,source 静默失败而 doctor 仍报通过 #661 uninstall case. - Extracting
isWindowsFormPathwas the right call, and worth a note: the defect you hit in CI is not observable on Windows at all. On a Windows hostenvShPath.split(path.sep)and a shape-based check agree, so only a POSIX runner separates them. Windows-only testing would have shipped it green — good catch by CI, and the shape-based check is what makes the test meaningful on both hosts.
One incidental observation, not caused by this change
teamai uninstall left ~/.teamai/ and its contents on disk (config.yaml, debug.log, env.sh, state.json, .sync-lock) in 5 of 6 scenario runs. This reproduces on both commits, so it is pre-existing and unrelated to this PR — one run did print Removed …\.teamai/, so it looks intermittent rather than unconditional. Flagging it only so it is not mistaken for a regression from this change; I have not investigated it further.
|
Findings
The PR description includes a detailed test plan and real-CLI end-to-end verification, so no testing-documentation finding is required. |
Two findings from the bot's re-review of 5f762ac: - P1: pull.ts counted every failed check the same way, so the new informational "No stale env blocks left behind" check still made a genuinely healthy delivery print "Pull finished, but 1 check(s) failed" — the exact upgrade scenario this PR targets. Check now carries an `informational` flag; pull's post-pull summary excludes it from the failure count and prints it on its own line instead. doctor's own exit code is unaffected, since a stray leftover is still something worth acting on. - P2: the stray-block scan compared `shellProfilePath` against its generated candidate with raw `===`, so a valid override spelled with forward slashes (or, on Windows, different case) reported the block's own file as a stray copy of itself. Added `sameFile()`, resolved through `path.win32`/`path.posix` explicitly (so the win32 branch is actually exercised on ubuntu/macos CI, not just locally). Verified both fixes end-to-end on a real Windows 11 host: built dist/index.js, drove it against a scratch $HOME and a real local git team repo with a legacy .bashrc block plus a working .profile block (forward-slash shellProfilePath) — only the stray .bashrc block is named, no "check(s) failed" banner, and .profile is not misreported as a stray copy of itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed both in P1 ( Real-machine re-verification of the exact upgrade scenario (built No "check(s) failed" banner; the leftover is still named. P2 ( Re-verified on the same real Windows box with Added regression tests for both: |
The PR description includes both a detailed test plan and real-CLI end-to-end verification, so no testing-documentation finding is needed. |
…(review) The bot's round-7 review found a real flip-flop: Git for Windows' /etc/profile.d/bash_profile.sh auto-generates ~/.bash_profile (a plain file sourcing ~/.bashrc, not a symlink) the first time a login shell starts with .bashrc present but none of .bash_profile/ .bash_login/.profile. That satisfies exactly the condition a first pull leaves behind (.bashrc written, nothing else exists yet), so the very next login shell creates .bash_profile out from under it. detectShellProfile()'s order then prefers that newly-existing file on the *next* pull, injecting a second block there and reporting the original .bashrc block — still loading, just one hop further away — as a dead leftover. Added resolveActiveShellProfile() in shell-profile.ts: before falling back to the order-based detectShellProfile(), it checks whether any candidate already carries a block for this scope's env.sh and, if so, reuses it. Both the write path (EnvHandler.pullItem, via detectShellProfile) and the read path (doctor-delivery's envDeliveryProblems) now resolve through it, so pull and doctor keep agreeing on the same file. (The bot's second finding described the fix as "resolve real paths / compare file identity" — .bash_profile isn't a symlink or hardlink to .bashrc, it's a regular file with a `source ~/.bashrc` line in it, so realpath() wouldn't touch this at all. Confirmed the exact generated content directly from a local Git for Windows install (etc/profile.d/bash_profile.sh) before designing the fix around that.) Verified end-to-end on a real Windows host: two `teamai pull` runs against a real local git team repo, with the exact Git-for-Windows forwarding .bash_profile content dropped in between (not written by teamai) — second pull updates .bashrc in place, no duplicate block, `teamai doctor` reports both env checks clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed in Confirmed the underlying mechanism first rather than assuming: checked So the P1 is real and exactly as described: a first pull with nothing on disk yet writes into One correction on the second finding's framing: Fix: Real-machine verification of the exact sequence: pull #1 (nothing on disk) → No duplicate block, no stale-block warning. Added regression tests at three levels:
|
|
FYI: |
|
Findings
Testing
|
|
Good catch — real, serious bug. Fixed in `1ed0141`. `02c93fe`'s `resolveActiveShellProfile` scanned all of `SHELL_PROFILE_CANDIDATE_NAMES` in a fixed order and returned the first candidate carrying a matching block — broader than the Git-for-Windows-forwarding case it was written for. Since `.bashrc` sorts before `.profile` in that list, a stale pre-#682 block left in `.bashrc` would outrank a correctly order-picked, not-yet-written `.profile` — exactly the upgrade scenario reported, and exactly the original #682 bug, reintroduced silently: worse than before, since `doctor` no longer caught it (the stale block is well-formed where it sits, so `envBlockSourcesPath` on it reports true). Fix: reworked to start from `detectShellProfile`'s order-based pick — the file the current environment actually reads — and only diverge from it when that pick's own content references another candidate by a home-relative path (`~/.bashrc`, `$HOME/.bashrc`), the shape a real sourcing line takes. A block sitting in a candidate the pick never reaches is no longer preferred over the pick, no matter what it contains. Also worth flagging on myself: the first cut of that reference check was a bare substring match on the filename, and my own test's plain-English comment (`"...unrelated to .bashrc"`) satisfied it and failed the test — caught before push, tightened to require the actual `~/name` / `$HOME/name` shape. Real-machine re-verification of both scenarios: The reported upgrade case (stale `.bashrc` block, empty `.profile`, no forwarding between them): The prior round's Git-for-Windows forwarding case, re-run to confirm no regression: Replaced the test that had asserted the old (incorrect) broad-stickiness behavior, and added the upgrade-case regression test. `npx tsc --noEmit` clean; full `vitest run` at the same pre-existing 30-file/70-test Windows-host-only baseline — no new failures. |
Squash-rebased onto the latest upstream/main to resolve the PR's merge conflict (main gained Tencent#693/Tencent#685/Tencent#694/Tencent#691/Tencent#681/Tencent#680/Tencent#666 since this branch forked). This combines all commits from the PR into one, applied cleanly on top of the new base — no functional changes from the previously reviewed state. The only real conflict was in src/__tests__/uninstall.test.ts, where diff3 split a test mid-body because of the repeated `});` boilerplate around it; resolved by keeping both sides' new tests intact, in full.
…y (review) Two more P1s from the bot's round-10 review of 75f3eac: - The traversal committed to the first referenced candidate in SHELL_PROFILE_CANDIDATE_NAMES's fixed priority order and gave up if that branch was a dead end, instead of trying every candidate the current file actually references. Git for Windows' own generated .bash_profile sources both .bashrc and .profile in one file — if the real block sits in .profile but .bashrc (sorting earlier) has none, the walk stopped at .bashrc without ever trying .profile. Reworked into a breadth-first search over the whole reference graph. - Splitting statements on `||` treated its right side as unconditionally reached, but `||`'s right side only runs if the left side fails, which isn't something this code can establish. `source ~/.profile || source ~/.bashrc` would mark .bashrc reachable even when .profile succeeds. Statements no longer split on `||`; a `source`/`.` sitting only after it is folded into its left side's statement and never recognized as its own reference, so it's never preferred over a block the left side already reaches. Conservative by construction: worst case is falling back to the order-based pick (the pre-Tencent#693-fix behavior), never a false "reachable". Verified the exact branching scenario end-to-end on a real Windows host: .bash_profile with the literal Git-for-Windows-generated content (sources both .bashrc and .profile), .bashrc empty, real block in .profile — resolves to .profile, no duplicate, doctor fully clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s (review) Two more P1s from the bot's round-11 review of aaa1142, both about referencesCandidate() trusting shell control flow it can't actually evaluate: - Any `&&` was treated as making its right side reachable, without checking what the left side's condition even was. A guard like `[ "$TERM_PROGRAM" = vscode ] && source ~/.bashrc` would mark .bashrc reachable unconditionally, even though it only runs inside VS Code. Also flagged: a source sitting inside a multiline `if` body looks, line by line, identical to a top-level one. - Folding `||`'s right side into its left statement (the round-9 fix) went too conservative the other way: `source ~/.profile || source ~/.bashrc` DOES guarantee .bashrc runs when ~/.profile doesn't exist, and the resolver was never even trying it. Rather than growing another ad hoc regex tweak, rewrote referencesCandidate() around what it can actually verify without a real shell parser: - Unconditional: a bare `. REF` / `source REF` — but nothing inside an `if` block counts, conditional or not. An `if`'s condition is opaque to a line scanner; trusting some conditions and not others would just be guessing. - Existence-gated `&&`: only the self-referential idiom `test -f REF && . REF` / `[ -f REF ] && . REF`, where the tested path and the sourced path are the same candidate — the one `&&` condition this code can independently verify, by visiting that candidate itself later in the search. - `||` fallback: the left side always counts (always attempted); the right side counts only when the left side's own target file does not exist on disk — the one case an `||` fallback is actually guaranteed to run. Anything this can't resolve either way is never trusted: the search just doesn't queue that candidate, and the caller falls back to the order-based pick — at worst a harmless duplicate block (the pre-Tencent#693-fix behavior), never a false "reachable" that would reintroduce Tencent#682. Verified end-to-end on a real Windows host: re-ran the core Git-for-Windows two-pull scenario from Tencent#693 (self-referential &&, still recognized) with no regression. Added 3 unit tests for the new boundaries: || recognized when the left target is missing, a non-existence && condition rejected, and a source nested inside an if block rejected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…es (#682) (#715) * fix(env): only stick to a candidate the active profile actually reaches (review) 02c93fe's resolveActiveShellProfile scanned every SHELL_PROFILE_CANDIDATE_NAMES entry for a matching block and returned the first hit, in a fixed order (.zshrc, .bashrc, .bash_profile, .bash_login, .profile). That's broader than the Git-for-Windows-forwarding case it was written for: a stale block a pre-#682 install left in .bashrc would outrank a correctly order-picked .profile that hasn't been written to yet, since .bashrc sorts earlier in the candidate list — silently reintroducing #682 for exactly the installs upgrading through this fix, with doctor unable to catch it because the stale block is well-formed where it sits. Reworked to start from detectShellProfile's order-based pick (the file the current environment actually reads) and only diverge from it when that pick's own content references another candidate by a home-relative path (~/.bashrc, $HOME/.bashrc) — the shape Git for Windows' generated forwarding file actually takes. A block sitting in a candidate the pick never reaches is no longer preferred over the pick, regardless of what it contains. Also caught and fixed a case of exactly the failure mode this PR is about: the first cut of the forwarding check was a bare substring match on the candidate's filename, and my own test's plain-English comment ("...unrelated to .bashrc") satisfied it. Tightened to require the home-relative reference form a real sourcing line uses. Verified both scenarios end-to-end on a real Windows host: - The exact bot-reported upgrade case (stale .bashrc block, empty .profile, no forwarding between them): pull now writes into .profile and correctly flags .bashrc as stale; doctor reports delivery healthy. - The Git-for-Windows forwarding case from the prior round: still sticks to .bashrc through the generated .bash_profile, no duplicate, no stale-block warning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): match real source commands, resolve reachability transitively (review) Two P1s from the bot's review of #715: - resolveActiveShellProfile's reachability check was a bare substring search on the active pick's content. A comment mentioning a filename (never executed) or a longer file sharing the same prefix (~/.bashrc.local) would both satisfy it, letting a stale block win the same way #682 did. Replaced with referencesCandidate(): strips full-line comments, splits each remaining line into statements on &&/||/;, and only counts a statement whose first word is literally `.` or `source` and whose second word is an anchored home-relative reference to exactly that candidate. - The check only followed one hop: .bash_profile sourcing .profile sourcing .bashrc (the common Debian .profile pattern, sourcing .bashrc for interactive shells) would miss a block two hops away and inject a duplicate. Reworked into a loop that walks the chain of files the pick actually sources, with a visited set for cycle protection, stopping at the first one that carries the block. Also fixed the P2: EnvHandler.detectShellProfile's doc comment still claimed it "stays on whichever candidate already carries this scope's block" unconditionally, which stopped being true once reachability was required. Verified end-to-end on a real Windows host: - The new two-hop chain (.bash_profile -> .profile -> .bashrc, block in .bashrc): resolves to .bashrc, no duplicate, doctor fully clean. - Re-ran the Git-for-Windows one-hop scenario and the #682 upgrade scenario from the prior round — both still correct, no regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): search every referenced candidate, respect || conditionality (review) Two more P1s from the bot's round-10 review of 75f3eac: - The traversal committed to the first referenced candidate in SHELL_PROFILE_CANDIDATE_NAMES's fixed priority order and gave up if that branch was a dead end, instead of trying every candidate the current file actually references. Git for Windows' own generated .bash_profile sources both .bashrc and .profile in one file — if the real block sits in .profile but .bashrc (sorting earlier) has none, the walk stopped at .bashrc without ever trying .profile. Reworked into a breadth-first search over the whole reference graph. - Splitting statements on `||` treated its right side as unconditionally reached, but `||`'s right side only runs if the left side fails, which isn't something this code can establish. `source ~/.profile || source ~/.bashrc` would mark .bashrc reachable even when .profile succeeds. Statements no longer split on `||`; a `source`/`.` sitting only after it is folded into its left side's statement and never recognized as its own reference, so it's never preferred over a block the left side already reaches. Conservative by construction: worst case is falling back to the order-based pick (the pre-#693-fix behavior), never a false "reachable". Verified the exact branching scenario end-to-end on a real Windows host: .bash_profile with the literal Git-for-Windows-generated content (sources both .bashrc and .profile), .bashrc empty, real block in .profile — resolves to .profile, no duplicate, doctor fully clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): only trust verifiable && and || conditions, ignore if bodies (review) Two more P1s from the bot's round-11 review of aaa1142, both about referencesCandidate() trusting shell control flow it can't actually evaluate: - Any `&&` was treated as making its right side reachable, without checking what the left side's condition even was. A guard like `[ "$TERM_PROGRAM" = vscode ] && source ~/.bashrc` would mark .bashrc reachable unconditionally, even though it only runs inside VS Code. Also flagged: a source sitting inside a multiline `if` body looks, line by line, identical to a top-level one. - Folding `||`'s right side into its left statement (the round-9 fix) went too conservative the other way: `source ~/.profile || source ~/.bashrc` DOES guarantee .bashrc runs when ~/.profile doesn't exist, and the resolver was never even trying it. Rather than growing another ad hoc regex tweak, rewrote referencesCandidate() around what it can actually verify without a real shell parser: - Unconditional: a bare `. REF` / `source REF` — but nothing inside an `if` block counts, conditional or not. An `if`'s condition is opaque to a line scanner; trusting some conditions and not others would just be guessing. - Existence-gated `&&`: only the self-referential idiom `test -f REF && . REF` / `[ -f REF ] && . REF`, where the tested path and the sourced path are the same candidate — the one `&&` condition this code can independently verify, by visiting that candidate itself later in the search. - `||` fallback: the left side always counts (always attempted); the right side counts only when the left side's own target file does not exist on disk — the one case an `||` fallback is actually guaranteed to run. Anything this can't resolve either way is never trusted: the search just doesn't queue that candidate, and the caller falls back to the order-based pick — at worst a harmless duplicate block (the pre-#693-fix behavior), never a false "reachable" that would reintroduce #682. Verified end-to-end on a real Windows host: re-ran the core Git-for-Windows two-pull scenario from #693 (self-referential &&, still recognized) with no regression. Added 3 unit tests for the new boundaries: || recognized when the left target is missing, a non-existence && condition rejected, and a source nested inside an if block rejected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(env): document transitive chaining and the verifiable-reference boundary (review) Round-11 P2: both docs described only a single directly-referenced candidate, but the resolver has followed transitive chains since 75f3eac and now only trusts specific verifiable && / || forms (60a2da0). Describes the Debian .profile -> .bashrc two-hop case alongside the Git-for-Windows one, and names the three reference shapes recognized (bare source, self-referential existence-gated &&, existence-checked || fallback) and that if-bodies are never trusted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): validate reference quoting, credit &&'s left side, generalize block-skip (review) Round 12 found three more real gaps in referencesCandidate(), plus one I agree isn't worth chasing further (see PR reply): - The quote check accepted `source "~/.bashrc"` and `source '$HOME/.bashrc'` as valid references, but a shell never tilde-expands inside any quotes and never variable-expands inside single quotes — both source a literal, near-certainly nonexistent path. Tightened to the three forms that actually expand: bare `~/name`, and `$HOME/name` either bare or double-quoted. - `&&`'s left side is always attempted, the same as `||`'s — `source ~/.bashrc && echo ready` does reach .bashrc regardless of the trailing command, but the old "whole statement must be exactly `. REF`" check missed it. The leftmost command before the first `&&` (or no `&&` at all) is now checked the same way `||`'s left side already was. - Only `if`/`fi` was tracked, so a source inside an uncalled function, a non-selected `case` arm, or a loop body — none of them any more guaranteed to run than an `if` body — was wrongly treated as top-level. Generalized the "don't trust it" depth counter to cover for/while/until, case/esac, and function/brace groups too, sharing one counter since we only need to know whether we're inside *any* of them, not which one. Declined to extend if-body trust to cover the standard nested Debian `.profile` template (`if [ -n "$BASH_VERSION" ]; then if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc"; fi; fi`) — doing so would mean trusting the outer `$BASH_VERSION` check, which is exactly the class of unverifiable shell condition this design has refused since round 11. Fixed the docs instead: they previously (incorrectly) claimed this exact template was recognized; now they say plainly that nested conditionals of any kind fall back to the order-based pick. Verified end-to-end on a real Windows host: re-ran the Git-for-Windows two-pull scenario unaffected. Added 6 unit tests for the new boundaries (invalid vs. valid quoting, &&'s left side, function/case/ loop bodies). 41/41 in shell-profile.test.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): handle comments, line continuations, heredocs, and chained && in profile scanning (review) Round 13 review found five genuine structural gaps in referencesCandidate, all fixed by moving open/close-block detection to per-statement (post `;`-split) instead of per-line, and adding a logicalLines() preprocessing pass: - Backslash-continued lines were scanned independently, losing the conditional context of the line they continue (`cond && \` followed by `source X` on the next line looked unconditional). - A one-line `if ...; then ...; fi` only incremented depth (matched via the whole-line "opens" check) and never saw its own `fi` close it, permanently disabling recognition of every later unconditional source in the file. Two-line function definitions (`fn()` then `{` on its own line) double-incremented for the same reason. - The existence-gated `&&` guard was fully anchored, so a guarded source followed by further `&&`-chained commands (`[ -f X ] && . X && export Y`) didn't match even though the guard still holds. - Comment stripping only skipped whole-comment lines; a comment following a semicolon on the same line was still split into a "real" statement. - Heredoc bodies were scanned as literal executable lines. Declined the sixth (recognizing the Debian/Ubuntu nested `if [ -n "$BASH_VERSION" ]; then if [ -f ... ]; then . ...; fi; fi` template) for the same reason given in review round 12: the outer condition is unverifiable without a real shell, and this resolver's explicit, repeatedly-restated design boundary is to never trust an unverifiable condition — falling back to the order-based pick (a harmless duplicate block) is the intended safe behavior there, not a bug. Verified with 6 new unit tests (47/47 passing) plus a standalone real-fs script driving the actual resolveActiveShellProfile against a scratch HOME for all seven round-13 scenarios (all pass). Full suite unchanged at the pre-existing 30-failed-file/66-failed-test Windows-host baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(env): quote-aware statement splitting, subshells, dead code after return/exit, N-way || (review) Round 14 review found six more genuine gaps in referencesCandidate, all fixed: - The `;`/`&&`/`||` splits were plain `String.split`, so a separator character inside a quoted argument (e.g. `printf '%s' 'x; source ~/.bashrc; y'`) was treated as a real statement boundary, inventing an executed source out of string data. Added splitTopLevel(), a small quote-aware splitter (tracks single/double-quote spans, skips separators inside them) used everywhere a naive split was previously used. - `(...)` subshells weren't tracked as an unverified-block construct — a source inside one always runs, but its exports never reach the caller, so it must not count as reaching a candidate any more than an `if` body does. Added to opensUnverifiedBlock/closesUnverifiedBlock alongside the existing if/for/while/until/case/function handling. - An unconditional, top-level `return`/`exit` ends the file's control flow right there; anything textually after it was still being scanned as if reachable. Added a `halted` flag set on a bare return/exit statement, gating everything after it for the rest of the scan. - `sourceOf` required the source's argument to be the entire statement, so `. "$HOME/.bashrc" 2>/dev/null` and `source ~/.bashrc extra_arg` (both valid, both really sourcing the target) went unrecognized. Relaxed to capture just the first argument and allow anything after it. - The `||` fallback only handled exactly two operands — a three-way chain like `source ~/.profile || source ~/.bash_login || source ~/.bashrc` wasn't recognized at all, not even the always-attempted left side. Generalized to N operands: each one counts only when every operand before it is a recognized source whose target is verifiably missing from disk. - Multiple heredocs opened by one command (`cat <<A <<B`) only tracked one terminator, so the second heredoc's body was scanned as real statements once the first terminator was seen. heredocEnd is now a queue of terminators consumed in order. Declined the seventh finding again (the Debian/Ubuntu nested `if [ -n "$BASH_VERSION" ]` template) for the same reason given in rounds 12 and 13: the outer condition is unverifiable without a real shell, and this resolver's explicit design boundary is to never trust one — the order-based-pick fallback (a harmless duplicate block) is the intended safe outcome there, not a bug. Verified with 8 new unit tests (55/55 passing) plus a standalone real-fs script driving the actual built resolveActiveShellProfile for all nine round-14 scenarios (all pass, including confirming the Debian pushback case is unchanged). Full suite unchanged at the pre-existing 30-failed-file/66-failed-test Windows-host baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(env): replace the growing ad-hoc shell scanner with a narrow, closed recognizer (review) Round 15 review found nine more genuine parsing bugs, most of them direct consequences of the general-purpose statement/operand machinery added in rounds 13-14 (quote/escape-aware `;`/`&&`/`||` splitting, N-way `||` chains, trailing-argument tolerance on `source`). It also included a meta-finding, correctly: this had grown into a large, incomplete ad-hoc shell parser for what should be a narrow forwarding-detection case, and each round's fix was mostly patching bugs the previous round's own machinery introduced. Full shell parsing is undecidable without a real shell; chasing it one adversarial regex at a time was never going to finish, and it was already producing real regressions (the round-14 `sourceOf` relaxation meant to recognize valid trailing arguments also started recognizing `source ~/.bashrc | cat` and `source ~/.bashrc &`, both of which run in a subshell and never actually reach the caller). Replaced `referencesCandidate`'s open-ended grammar with a closed recognizer of exactly two forms, each matched as a complete logical line: - bare unconditional `. REF` / `source REF` - the self-referential existence guard `test -f REF && . REF` / `[ -f REF ] && . REF` — the literal line Git for Windows itself generates Deleted entirely: quote/escape-aware statement splitting (no longer needed — nothing is split into statements anymore), `||` fallback handling (both the original two-operand and round 14's N-way generalization), trailing-argument/redirection tolerance on `source` (the source of the pipe/background regression above), and comment-stripping (unnecessary now — a line with anything extra on it simply fails the exact-match check, which is a large part of why the statement machinery could be deleted rather than just patched again). Kept, since dropping them would reopen a real false-positive risk rather than just narrow scope: block-depth tracking for `if`/`for`/`while`/`until`/`case`/`select`/function/subshell/brace-group (content inside is either conditional or non-propagating, generalized this round with `select` and a fixed one-liner if/for/while/until/case collapse so a self-contained one-liner doesn't corrupt depth tracking for the rest of the file), heredoc body skipping (fixed three real bugs in it: a `<<<` here-string was mistaken for a `<<` heredoc and swallowed the rest of the file; a non-`-` heredoc's terminator was compared with `.trim()`, letting an indented look-alike end it early; the delimiter charset was `\w` only, missing real delimiters like `END-CONFIG`), a `return`/`exit` halt flag (cheap, and the alternative — textually dead code after an unconditional exit still being scanned — is a genuine false positive, however unlikely the pattern), and joining a line ending in `\`, `&&`, or `||` onto the next (real, unremarkable shell continuation with no backslash required for the latter two — the risk this closes isn't hypothetical: an unrelated trailing `&&` followed by an unconditional-looking `source` on the next line is exactly the shape that would have produced a false "reachable"). Declined the Debian/Ubuntu nested-`if` finding a fourth time, unchanged from rounds 12-14: the outer `$BASH_VERSION` check is unverifiable without a real shell, and this resolver's explicit boundary is that an unverifiable condition is never trusted. The `||`-existence-only pushback from round 14 is now moot — `||` isn't recognized in any form. Net change to shell-profile.ts is negative (-244/+something smaller) despite fixing more bugs than it added, confirming this is a real simplification rather than another round of patches. Verified with an updated unit test suite (61/61 passing — six tests for now-out-of-scope behavior replaced with tests confirming the safe fallback, new tests added for every round-15 fix that was kept) and a standalone real-fs script against the actual built resolver covering all twelve round-15 scenarios (all pass, including the real motivating Git-for-Windows case and the still-declined Debian template). Full suite unchanged at the pre-existing 30-failed-file/66-failed-test Windows-host baseline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(pi): add Pi Coding Agent integration Squash-rebased onto the latest upstream/main to resolve the PR's merge conflict (main gained #693/#685/#694/#691/#681/#680/#666 since this branch forked). This combines all commits from the PR into one, applied cleanly on top of the new base — no functional changes from the previously reviewed state. The only real conflict was in src/__tests__/uninstall.test.ts, where diff3 split a test mid-body because of the repeated `});` boilerplate around it; resolved by keeping both sides' new tests intact, in full. * fix(pi): gate agent-hook files on the same per-slug marker ownership check applyPiAgentHook()/removePiAgentHook() wrote and deleted teamai-agent-<slug>.ts purely by path, with no ownership check — the same class of bug already fixed for the main teamai-hooks.ts file, but never extended to the per-slug HTTP agent-hook files. A user-authored file at that conventional path could be silently overwritten on sync or deleted on uninstall. Adds hasPiAgentHook(slug), mirroring hasPiHooks: injection now skips (with a warning) instead of overwriting a same-named file without the `[teamai] agent hook [<slug>]` marker, and removal skips instead of deleting one. uninstall.ts's discovery scan now derives each file's slug and checks the same marker before scheduling it for removal, instead of matching by filename prefix alone. * fix(pi): fail install_hook_rule instead of silently acking a skipped Pi agent hook applyPiAgentHook warned and returned normally when the requested event has no Pi equivalent or a same-named extension file exists without the TeamAI marker. The caller in local-agent.ts wrote the manifest entry and acked success regardless, so the server and local state believed the hook was installed even though the file was never touched. Throw in both cases so the existing install_hook_rule error path acks failure instead. Also document the known limitation (shared with the OMP adapter) that a scoped Pi uninstall is not durable across multiple projects on the same machine, since the extension is one machine-wide file and hook dispatch has no per-project exclusion check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Problem
detectShellProfile()readsSHELLto decide between.zshrcand.bashrc.SHELLis never set on Windows, so it always falls back to~/.bashrc— but Git Bash starts as a login shell, which reads~/.bash_profile,~/.bash_loginor~/.profile, never~/.bashrc.teamai pullreports success, the block is written and looks correct on inspection, but no shell ever sources it. Nothing catches it:doctoronly checks that the block is present in the profile file, not whether that file is one the shell actually reads.Filed as #682.
Fix
src/utils/shell-profile.tswith the platform-aware logic:~/.bash_profile, then~/.bash_login, then~/.profile, in that order; fall back to~/.bashrconly when none exist — matching Git for Windows' own fallback in/etc/profile.d/bash_profile.sh, so both agree on what actually gets sourced.SHELL-based behavior.platformis an injectable parameter (defaultprocess.platform), same convention asresolveCliPathinutils/cli-path.ts— CI only runs ubuntu/macos, so a hardcodedprocess.platformwould leave the win32 branch permanently untested, which is how this went unnoticed in the first place.EnvHandler.detectShellProfile(resources/env.ts) and the standalone copy inuninstall.tsboth now delegate to this single implementation, instead of carrying two independent copies that could silently drift and leavepullanduninstalldisagreeing on which file to touch.sharing.env.shellProfilePath(the team-config override) is untouched — it already short-circuits beforedetectShellProfile()is called.Test Plan
Unit tests (
npx vitest run) — all passing:src/__tests__/shell-profile.test.ts: POSIX zsh/bash/unset-SHELL cases, and the win32 priority order (.bash_profile>.bash_login>.profile>.bashrcfallback), including the exact file combination reported in detectShellProfile() falls back to ~/.bashrc on Windows, where the login shell never reads it #682.env-handler.test.ts/uninstall.test.tsto pinprocess.platformto'linux'for their existing SHELL-based assertions, since I ran the full suite on an actual Windows dev machine and these tests otherwise silently started exercising the new win32 branch instead of the POSIX one they're meant to test (same reasoning as theplatforminjection above — this is precisely the coverage gap that let detectShellProfile() falls back to ~/.bashrc on Windows, where the login shell never reads it #682 through).npx tsc --noEmit: clean.Real CLI, end-to-end (
npm run build, then drove the builtdist/index.jsagainst a real local git team repo withHOME/USERPROFILEpointed at sandboxed fixtures — Windows-native paths, not Git Bash/tmp/...paths, which native Windows Node does not resolve):~/.bashrcpresent,~/.bash_profileabsent,~/.profilepresent):teamai pullnow injects into.profile, leaves.bashrcuntouched.teamai pullcreates and injects into.bashrc(matches Git for Windows' own fallback)..bash_profileand.bashrcexist:teamai pullprefers.bash_profile, leaves.bashrcuntouched.teamai doctorreports "Env variables injected in shell profile" as healthy against scenario 3.teamai uninstall --forceagainst scenario 3 correctly finds and cleans the block from.bash_profile(not.bashrc) — confirmingpull/doctor/uninstallnow agree on the same file, per the concern raised in the issue about the previously-duplicated logic.Note for reviewers
While running the full suite on Windows I found 30 pre-existing test files (70 tests, e.g.
doctor-env-delivery.test.ts,search-index-multi.test.ts,source.test.ts) that fail on an actual Windows host due to hardcoded POSIX path separators/characters in test fixtures — confirmed present on unmodifiedmainviagit stash(identical failure count before/after this change). Unrelated to this fix and invisible on the ubuntu/macos-only CI; flagging in case it's useful, happy to open a separate issue if not already tracked.Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com