Skip to content

fix(push): stop reverting a teammate's update from HOME or a stale .teamai copy (#823) - #835

Merged
jeff-r2026 merged 4 commits into
Tencent:mainfrom
SaulMoro:fix-823-push-bases
Sep 26, 2026
Merged

jeff-r2026 merged 4 commits into
Tencent:mainfrom
SaulMoro:fix-823-push-bases

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Push no longer offers a teammate's update back as a local edit from a user-scope install (HOME), or from a single-repo branch behind the default branch.

 push, before any pull
   pre-push sync base
-    project scope: this checkout's bases   user scope: lastPullRev only
+    resolveCheckoutBases(localConfig, state)   # one record per checkout, HOME included
+  (an inheriting project pull that moves HOME's copies adds its revision to HOME's bases)
   scan
     .teamai/rules/<rule>            (single-repo)
-      older team version: skipped only if this machine placed it
+      older team version: skipped with a warning
     .teamai/skills/<skill>          (single-repo)
-      older team version: listed as modified
+      every differing team file is an older version,
+      or missing and never added on this branch: skipped with a warning
  • Item 4: a user-scope pull records HOME in lastPullByWorkspace, and push reads and extends it the way it does for a project checkout (fix(push): keep a teammate's update when pushing from a stale worktree (#812) #819). The user-scope fast path still reads the shared fields. An install with no record yet keeps lastPullRev and is not refused: HOME is the scope's only checkout. Its first push creates the record from lastPullRev and adds the revision its sync reached.
  • Item 19: a project pull with inheritUserScope moves HOME's skills, rules and agents, so it adds its revision to HOME's push bases in the same record, creating the record from lastPullRev if there is none. It leaves the record's rev, lastPullRev and both fast paths alone.
  • Upgraded install without a HOME record: push's fallback bases are lastPullRev and lastInheritedPullRev, and the record created from them keeps lastInheritedPullRev as a push base. Nothing records which of the two pulls ran last, and each delivered every rule, skill and agent push compares, so both are candidates and a copy matching either counts as unedited. Picking one would re-open the bug whenever the other ran last.
  • A pull whose docs mirror or submodule update fails still delivered skills, rules and agents at the new revision. It now adds that revision to the checkout's push bases (HOME's or a project checkout's) and keeps the record's rev, lastPullRev = null and the retry exactly as before.
  • Item 10: the isPastVersionOf guard now covers every .teamai/rules copy, and .teamai/skills gets the same guard. A genuine edit is still listed, because its blob is not in the default branch's history. A team file missing from the .teamai/skills copy is a teammate's addition when the member's branch never added it; otherwise it is the member's deletion, and still listed.
  • A full pull that holds skills or agents on a namespace collision leaves their copies where they were, yet replaced the checkout's record with { rev, targets }, dropping the bases push compared those copies with. It now keeps them as pushBaseRevs, read before the marker moves, for HOME and for a recorded project checkout (items 4 and 19; the same revert, through a held pull).
  • The replaced-root-rule cleanup (isDeliveredRender in rules.ts) compared a copy only with the current render and the one at lastPullRev. It now accepts a render at any of the checkout's bases, so an unedited copy an inherited pull delivered, or a project checkout whose record differs from the shared lastPullRev, is withdrawn instead of kept with a "differs from what teamai delivered" warning.
  • resolveCheckoutBases(localConfig, state): Promise<CheckoutBases> (exported from pull.ts, beside checkoutBaseRevs) replaces the copies of this logic in push.ts and agents.ts; project scope behaves as before.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run passes (326 files, 5045 passed, 1 skipped)
  • Added/updated tests for the change: 15 e2e cases in push-sync-followups-823.test.ts, 1 unit case in pull-sync-truth.test.ts, 1 unit case in pull-namespace-override.test.ts
stale .teamai/rules copy, branch behind main    main: listed "(modified)"  -> held with warning
stale .teamai/skills copy (+ member-only file)  main: listed "(modified)"  -> held with warning
stale skill lacking a file a teammate added     main: listed "(modified)"  -> held with warning
genuine edit of either, or deleted branch file  listed "(modified)" before and after
user scope: push synced HOME to R2, teammate R3 main: listed "(modified)"  -> updated to R3
user scope with no record (upgrade)             fast path, lastPullRev, edit listed, no refusal
upgrade: push synced HOME to R2, teammate R3    main: listed "(modified)"  -> updated to R3
inherited project pull moved HOME to R2, R3     main: listed "(modified)"  -> updated to R3
  (with a user-scope record, and without one)
same, an older CLI ran that pull (no record)    ad0ce83c: listed "(modified)" -> updated to R3
user pull to R2 with docs mirror failing, R3    ad0ce83c: listed "(modified)" -> updated to R3
project checkout, docs mirror failing (unit)    ad0ce83c: no base         -> rev kept, base added
held skill (collision), teammate resolves + S3  32bffbdb: listed "(modified)" -> updated to S3
  (user scope and project checkout)
replaced root rule at an inherited pull's rev    32bffbdb: kept + warning  -> withdrawn

Ablations (each reverted alone, e2e rerun): the user-scope record key, the rules guard, the skills guard, the added-file check (both directions), the project-only refusal, the user-scope fast path, the inherited pull's base, the record it creates when there is none, and the record an upgraded install's first push creates each turn their test red. Creating that record with an empty rev instead of lastPullRev stays green: an inherited pull delivers every rule and skill push compares, so keeping lastPullRev only makes the bases a superset of what push read before.

Review cycle 2: the three new cases are red on ad0ce83c (the pull.ts fix swapped out) and green after. Full npx vitest run passes; e2e push-sync-followups-823, push-stale-worktree-812, pull-new-worktree-807, roles-tags-pull, pull-missing-team-config, import-mr-publish-823 (6 files, 49 passed).

Review cycle 3: the held-skill e2e (both scopes) and the inherited-pull row of the replaced-root-rule unit test are red on 32bffbdb and green after. npx tsc --noEmit, full npx vitest run and npm run build pass; e2e push-sync-followups-823, push-stale-worktree-812, roles-tags-pull, project-scoped-delivery, namespaced-entries, pull-new-worktree-807 (6 files, 46 passed).

Earlier e2e run with --retry 0: push-sync-followups-823, push-stale-worktree-812, pull-new-worktree-807, self-mode-worktrees-808, role-scoped-agents, namespaced-entries, project-agent-cold-start, scope-isolation-issue85, deleted-worktree-scope-810, multi-project, scope-inheritance-e2e, e2e (12 files, 107 passed, 22 skipped by their own guards).

Real CLI: teamai --dry-run push, origin/main vs this branch

Throwaway sandboxes with local bare remotes; the config files are written by hand, as the e2e harness does.

Item 10: single-repo project, a teammate lands a new .teamai/rules/team-rule.md on main, the member fetches and does not merge.

# before (origin/main 7c834ce4)
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/project/.teamai/rules/team-rule.md

# after (52794183)
⚠ [rules] Skipped team-rule: .teamai/rules/team-rule.md is an older version of rules/team-rule.md, which has changed on the team since. Copy the current file over it (or delete it) before editing.
ℹ No new or modified resources to push

Item 4: user scope, pull R1, teammate R2, --dry-run push (syncs HOME to R2), teammate R3, --dry-run push again.

# before
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/home2/.claude/rules/team-rule.md
# ~/.claude/rules/team-rule.md: Version two, from a teammate.
# user state lastPullByWorkspace: 0 record(s)

# after
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version three, from a teammate.
# user state lastPullByWorkspace: 1 record(s), pushBaseRevs=2

Item 19: user scope pull R1; a project with inheritUserScope: true pulls R2; teammate R3; user-scope --dry-run push.

# before (52794183, item 4 alone)
# ~/.claude/rules/team-rule.md after the project pull: Version two, from a teammate.
# user state lastPullByWorkspace: 1 record(s), pushBaseRevs=0
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/home/.claude/rules/team-rule.md

# after
# user state lastPullByWorkspace: 1 record(s), pushBaseRevs=1
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version three, from a teammate.

Item 4, upgraded install (review fix): user scope, pull R1, drop the user-scope record, teammate R2, --dry-run push, teammate R3, --dry-run push again.

# before (6c46ee46)
# user state lastPullByWorkspace: 0 record(s), pushBaseRevs=0
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version two, from a teammate.
# user state lastPullByWorkspace: 0 record(s), pushBaseRevs=0
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/home/.claude/rules/team-rule.md

# after (ad0ce83c)
# user state lastPullByWorkspace: 0 record(s), pushBaseRevs=0
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version two, from a teammate.
# user state lastPullByWorkspace: 1 record(s), pushBaseRevs=1
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version three, from a teammate.
# user state lastPullByWorkspace: 1 record(s), pushBaseRevs=2

Review cycle 2, upgraded install: user scope pull R1; a project with inheritUserScope: true pulls R2; drop the HOME record (an older CLI ran that pull); teammate R3; user-scope --dry-run push.

# before (ad0ce83c)
# ~/.claude/rules/team-rule.md after the project pull: Version two, from a teammate.
# user state: lastPullRev=set, lastInheritedPullRev=set, 0 record(s), pushBaseRevs=0
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/home/.claude/rules/team-rule.md
# ~/.claude/rules/team-rule.md: Version two, from a teammate.

# after (32bffbdb)
# user state: lastPullRev=set, lastInheritedPullRev=set, 0 record(s), pushBaseRevs=0
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version three, from a teammate.
# user state: lastPullRev=set, lastInheritedPullRev=set, 1 record(s), pushBaseRevs=2

Review cycle 2, docs mirror failing: user scope with a team doc, pull R1; a file blocks ~/.teamai/docs; teammate R2; pull; teammate R3; --dry-run push.

# before (ad0ce83c)
✔ [user] Synced 1 rule(s)
⚠ [user] Failed to sync docs: EEXIST: file already exists, mkdir '<sandbox>/home/.teamai/docs'
# user state: lastPullRev=null, lastInheritedPullRev=unset, 1 record(s), pushBaseRevs=0
Found 1 resource(s) to push:
    1. [rules] team-rule (modified)
       from: <sandbox>/home/.claude/rules/team-rule.md
# ~/.claude/rules/team-rule.md: Version two, from a teammate.

# after (32bffbdb)
✔ [user] Synced 1 rule(s)
⚠ [user] Failed to sync docs: EEXIST: file already exists, mkdir '<sandbox>/home/.teamai/docs'
# user state: lastPullRev=null, lastInheritedPullRev=unset, 1 record(s), pushBaseRevs=1
ℹ No new or modified resources to push
# ~/.claude/rules/team-rule.md: Version three, from a teammate.

Review cycle 3, held skill: user scope with role dev over namespaces alpha and beta, pull S1; a teammate updates alpha/team-skill and adds beta/team-skill; pull; the teammate removes beta and publishes S3; --dry-run push.

# before (32bffbdb)
⚠ [user] Duplicate skill "team-skill" found in active namespaces "alpha" and "beta" (skills/alpha/team-skill and skills/beta/team-skill). Skills were not updated this run; the installed ones are kept.
    1. [skills] team-skill (modified)
       from: <sandbox>/home/.claude/skills/team-skill
       to:   skills/alpha/team-skill
# ~/.claude/skills/team-skill/SKILL.md: Version one.

# after (5009e8fa)
⚠ [user] Duplicate skill "team-skill" found in active namespaces "alpha" and "beta" (skills/alpha/team-skill and skills/beta/team-skill). Skills were not updated this run; the installed ones are kept.
ℹ No new or modified resources to push
# ~/.claude/skills/team-skill/SKILL.md: Version three.

Related Issues

Part of #823 (items 4, 10 and 19)

Notes for Reviewers

Door: two-way. Blast radius: push.

  • A user-scope install upgraded from a CLI without the record writes it on its first push or next full pull (an already-synced pull, inherited or not, writes no state); until then push compares with lastPullRev, as today.
  • A deliberate revert of a .teamai/ rule or skill to an older team version is held with a warning, the trade-off already accepted for placed rules ([bug] Namespace resolution is inconsistent across resource types: push ignores --role/--project for rules and agents, and root-level claudemd never ships #649).
  • Placed agents in user scope now compare with HOME's record too, as project-scope agents already do (fix(push): stop offering a teammate's update back as a local edit (#823) #827).
  • Local adversarial review (the codex-review-on-assign.yml prompt, read-only Claude subagent) and /code-review (standards, spec): no blocking finding. Fixed here: a stale skill lacking a file a teammate added was still listed, and pushing it would have deleted that file.
  • The user-scope record was written only for lastPullRev pulls; an inherited pull now writes push bases into it too, but never its rev, which only a full user-scope pull sets. A pull whose docs mirror or submodule update fails does the same: it adds a base and leaves rev and the shared marker for the retry. A project checkout no pull had recorded gets a record with an empty rev in that case, since its copies are now at a known revision.
  • Review notes: fixed the upgraded install's first push saving no HOME base (P1), and documented the stale .teamai/ copy warning and its recovery in skill-data/core/references/contribute-member.md (P2); docs/designs/data-directory-layout.md covers the new record. Cycle 2: fixed the upgraded install ignoring lastInheritedPullRev (P1) and a failed docs mirror dropping the delivered base (P1, submodule failures had the same gap and share the fix). Scanned the other base and lastPullRev writes in pull.ts and push.ts: push already records its base when the sync stops partway; it records none when the team repo refresh failed, by design, since the tree may not match HEAD. Cycle 3: the two adjacent gaps first logged as follow-ups are fixed here, as they are the same revert class: a pull that holds skills (or agents) on a namespace collision keeps the older bases, and the replaced-root-rule cleanup reads every base. A project checkout no pull has recorded keeps no held bases, matching push's rule for that case (its fallback lastPullRev may be another checkout's). The held-agents path shares the flag and has no separate test.
  • Not covered: agents in .teamai/agents (unchanged), and extra providers and agents, which are left to CI.

…eamai copy (Tencent#823)

Item 4, user scope: push compared HOME's rules and skills with the shared
lastPullRev only, because the per-checkout push bases of Tencent#819 were keyed for
project scope alone. After a push synced HOME's unedited copy to a teammate's
R2, the next push compared it with R1 and offered it back over the teammate's
R3. A user-scope pull now records HOME under checkoutKey(HOME) in the user
state.json, and push reads and extends it like a project checkout's. The
user-scope fast path still reads the shared fields, and an install with no
record yet keeps comparing with lastPullRev without the unrecorded-checkout
refusal: HOME is the scope's only checkout, so that revision is its own.

Item 19, inherited user scope: a project pull with inheritUserScope rewrites
HOME's skills, rules and agents under lastInheritedPullRev without moving the
user scope's push bases, so the next user-scope push offered a teammate's
newer update back the same way. That pull now adds its revision to HOME's
pushBaseRevs, creating the record from lastPullRev if there is none, and
leaves the record's rev, lastPullRev and the fast paths alone.

Item 10, single-repo: the active tree's .teamai/rules and .teamai/skills are
push sources, and on a branch behind the default branch they hold older team
versions nobody edited, which push listed as modified. The isPastVersionOf
guard that held only placed rules now covers every .teamai/rules copy, and
.teamai/skills gets the same guard: a skill is skipped with a warning when
every team file whose copy differs is an older version of it. A team file
missing locally is a teammate's addition when the member's branch never added
it, and the member's deletion otherwise; member-only files are ignored, as the
equality check already ignores them.

The checkout-base resolution that push and the agents scan each repeated
(key, record, checkoutBaseRevs, lastPullRev fallback) is now one exported
helper, resolveCheckoutBases, next to checkoutBaseRevs in pull.ts; pull uses
the same key for its record.
@jeff-r2026 jeff-r2026 self-assigned this Sep 26, 2026
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/push.ts:1035 only records the refreshed revision when bases.source === 'checkout'. For an upgraded user-scope install with no HOME record, the first push can sync HOME from R1 to R2 but saves no R2 base. If a teammate then publishes R3, the next push compares the R2 copy only against lastPullRev R1 and offers R2 as a local modification, recreating the revert this PR intends to prevent. Create/update the HOME record after a successful user-scope pre-push sync even when resolution initially used the shared fallback.
  • [P2 non-blocking] The runtime push behavior changed, but no corresponding skill-data/ guidance was updated. The repository instructions require behavior changes to update affected skills; the stale-copy warning and recovery workflow belong in skill-data/core/references/contribute-member.md:112.

The PR description includes sufficient unit, E2E, and representative real-CLI verification.

…stall (Tencent#823)

An upgraded user-scope install with no HOME record synced HOME from R1 to R2
on its first push but saved no base, because push recorded one only when the
bases came from a record. A teammate's R3 then made the next push compare the
R2 copy with lastPullRev R1 and offer it back. Push now creates HOME's record
from lastPullRev (userScopeRecord, shared with the inherited pull) and adds
the revision its sync reached. An unrecorded project checkout still records
nothing, since its fallback base may be another checkout's.

skill-data: contribute-member explains the stale .teamai copy warning and
how to publish an edit of such a copy.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/pull.ts:632 ignores lastInheritedPullRev when upgrading state without a HOME record. A pre-upgrade install can have lastPullRev = R1, lastInheritedPullRev = R2, and HOME files at R2; after a teammate publishes R3, user-scope push compares only against R1 and offers R2 as a local modification. Include the inherited revision in the user-scope fallback or migrate it into the HOME record.
  • [P1 blocking] src/pull.ts:1356 records the newly delivered HOME base only inside the !docsSyncFailed block. If skills/rules are updated to R2 and the later docs mirror fails, R2 is not recorded; after R3 lands, push again treats the untouched R2 copy as a local edit. Persist successfully delivered push bases independently from the overall pull revision marker.

The two earlier findings are resolved in the current diff: first-push HOME record creation was added, and the affected skill-data/ guidance was updated. The PR description includes sufficient unit, E2E, and real-CLI verification.

@github-actions

Copy link
Copy Markdown

No new findings.

  • Earlier findings are resolved by HOME record creation in src/push.ts:1041, inherited-revision fallback in src/pull.ts:633, partial-pull base persistence in src/pull.ts:1370, and updated guidance in skill-data/core/references/contribute-member.md:112.
  • The PR description documents sufficient unit, E2E, and representative real-CLI verification.
  • Per instruction, I reviewed the diff only and did not run or build PR code.

@github-actions

Copy link
Copy Markdown

No new findings.

  • Earlier findings are resolved by HOME record creation in src/push.ts:1041, inherited-revision fallback in src/pull.ts:633, incomplete-pull base persistence in src/pull.ts:1382, and guidance in skill-data/core/references/contribute-member.md:112.
  • The PR description documents sufficient unit, E2E, and representative real-CLI verification.
  • Per instruction, I reviewed only the specified diff and did not run or build the PR code.

@jeff-r2026
jeff-r2026 self-requested a review September 26, 2026 11:24
@jeff-r2026
jeff-r2026 merged commit 49675a9 into Tencent:main Sep 26, 2026
11 checks passed
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