Skip to content

fix(pull): do not self-heal the .gitignore under --dry-run - #944

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
Smilewithoutfalling:fix/pull-dry-run-gitignore
Oct 2, 2026
Merged

jeff-r2026 merged 1 commit into
Tencent:mainfrom
Smilewithoutfalling:fix/pull-dry-run-gitignore

Conversation

@Smilewithoutfalling

Copy link
Copy Markdown
Contributor

What

pull refreshes the team repo before it does anything else. In self mode that
refresh calls migrateSelfModeGitignore, which rewrites .teamai/.gitignore — a
tracked file in the member's own checkout — when it still has the pre-beta.5
shape (a bare env line that keeps team env vars off main).

refreshTeamRepo(localConfig) takes no options, so it cannot tell a preview from
a real run, and pull reaches it above every dry-run guard. pull --dry-run
therefore writes.

The refresh now takes options and runs the self-heal only when the run is real.
The migration is idempotent, so the next real pull performs it.

Evidence

Real CLI, self-mode clone, .teamai/.gitignore = token\nenv\nenv.local\n,
committed to the checkout:

.teamai/.gitignore sha256
before pull --dry-run ed747943d24dc463…
after pull --dry-run f572d520732a16c2…

Whole-fixture tree hash: exactly one path differed — app/.teamai/.gitignore —
and nothing was removed. pull reported no error.

Test

dry-run-load-path.test.ts already asserts, for a fresh self-mode clone, that
"nothing that already existed may be rewritten, whatever it is". Its fixture
(setupSelfModeClone) has no .teamai/.gitignore at all — and the migration
returns early when the file is missing — so the one path the refresh could write
to was the one path that fixture could not reach.

The new case adds the file to that same fixture and asserts the same thing:

pull.ts (blob) new case whole file
main 509a8135 fails — the file was rewritten 1 failed / 94 passed
this PR 7559d8d3 passes 95 passed / 0 failed

Relation to #866

This is the one [P1 blocking] finding from the review on #866 that I could still
reproduce on main today. The lock-directory half was fixed in #896, and the
queued-learning publish is gated there as well. I have not re-measured the
remaining two.

The line is the one the review pointed at: refreshTeamRepo →
migrateSelfModeGitignore (now at src/pull.ts:112).

`refreshTeamRepo` self-heals an older `.teamai/.gitignore` -- one that still
ignores a bare `env` (pre-beta.5). That rewrites a *tracked* file in the member's
own checkout, and `pull` reaches the call above every dry-run guard: the function
takes no options, so it cannot tell a preview from a real run.

Thread `options.dryRun` into the refresh and gate the self-heal on it. The
migration is idempotent, so the next real pull still performs it.

Measured with the CLI on a self-mode clone whose `.teamai/.gitignore` is the
pre-beta.5 shape and is committed:

  before pull --dry-run   .teamai/.gitignore sha256 ed747943d24dc463
  after  pull --dry-run   .teamai/.gitignore sha256 f572d520732a16c2

Whole-fixture tree diff: exactly one path changed, and it was that file.

This is the one finding from the review on Tencent#866 that I could still reproduce on
main today; the empty `locks/` directory it also reported is fixed in Tencent#896.
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
  • [P1 blocking] src/pull.ts:926 changes runtime behavior, but the PR description lacks a successful real-CLI verification of the fixed branch. Its real-CLI evidence only reproduces the original rewrite (the before/after hashes differ); the fix is validated only by Vitest. The Code Review Rules require one representative end-to-end/real-CLI verification record for runtime changes.

The earlier dry-run .gitignore rewrite finding is resolved by the new options.dryRun guard.

@jeff-r2026
jeff-r2026 merged commit b0ce1f2 into Tencent:main Oct 2, 2026
14 of 15 checks passed
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 2, 2026
Resolve refreshTeamRepo's options: keep inline/fetchTimeoutMs and add main's
dryRun guard for the self-mode .gitignore self-heal (Tencent#944).
@Smilewithoutfalling

Copy link
Copy Markdown
Contributor Author

Real-CLI verification for the --dry-run guard ([P1] on src/pull.ts:926)

The finding was right: #944's description carried only the unfixed tree reproducing the rewrite (before/after hashes differ) plus Vitest. That is a reproduction of the bug, not a verification of the fix. Here is the record that was missing. Posting it after the merge because this is when it exists.

Fixture — a self-mode project (.teamai/teamai.yaml, mode: self) with a committed, tracked .teamai/.gitignore in the pre-beta.5 shape token\nenv\nenv.local\n. Fresh repo per run, same directory name, same isolated HOME.

Command — the built CLI as a child process, nothing mocked:

node dist/index.js --dry-run pull      # cwd = the project

The only variable between the two runs is src/pull.ts.

base 509a8135 fix 7559d8d3
.teamai/.gitignore sha256 before ed747943d24dc463… ed747943d24dc463…
.teamai/.gitignore sha256 after f572d520732a16c2… ed747943d24dc463…
rewritten? true false
whole-tree rewritten paths ["app/.teamai/.gitignore"] []
git status --porcelain after M .teamai/.gitignore `` (clean)
CLI exit / signal 0 / none 0 / none

Content after the run — base: "token\nteamai.lock\nenv.local\nlearnings-wt/\n.learnings-lock\npending-learnings/\nusage.jsonl.*\nusage.pending-*.jsonl\nconfig.yaml.*.tmp\n"; fix: "token\nenv\nenv.local\n" (byte-identical to the pre-image). Both runs printed Team repo: single-repo (knowledge on main), so both reached the self-mode branch — the guard, not an early exit, is what changed the outcome.

Bridging this to main. The squash commit b0ce1f22 has parent bae48e5c, not the base this PR was opened on (83228692) — main moved while the PR sat, and GitHub applied the PR's three hunks onto the newer parent. Main's pull.ts is therefore 5acff3c6, not the blob I pushed, and it cannot be rebuilt in the tree I measured: it imports instructionFileInstallProbe / writesInstructionBlock, which upstream added to rule-format.ts after my base. Rather than hand-wave that gap, the four regions this run exercises were compared literally against main:

  • the doc-comment paragraph at src/pull.ts:83
  • the options: { dryRun?: boolean } = {} parameter at :93
  • the guard plus dynamic import at :121
  • the call site refreshTeamRepo(localConfig, options) at :926

All four are byte-identical in main and in the copy that was run. bae48e5c...b0ce1f22 records exactly +15/-5 for pull.ts, matching those hunks. The test file is byte-identical as well (715fc4d2116a81ac6df146221f1d55374f68532e on main == the local copy).

So the shipped code leaves the tracked file alone under --dry-run; the earlier rewrite is gone from base → fix, measured through the real CLI rather than through the harness.

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