Skip to content

fix(pull): deliver shared root skills when the team uses projects or roles - #1007

Open
EDM-luoZ wants to merge 1 commit into
Tencent:mainfrom
EDM-luoZ:fix/root-skills-with-projects-manifest
Open

EDM-luoZ wants to merge 1 commit into
Tencent:mainfrom
EDM-luoZ:fix/root-skills-with-projects-manifest

Conversation

@EDM-luoZ

@EDM-luoZ EDM-luoZ commented Oct 9, 2026

Copy link
Copy Markdown

What

For a member with no role and no active project on a team that has manifest/projects.yaml, resolveDesiredSkills returned an empty set: scanRoleAwareSkills only scanned namespace directories and never the shared skills/<name>/ root. Two consequences on every pull:

  1. Root skills stopped deploying (No resources to sync even with --force).
  2. The desired-union cleanup in pull.ts then pruned every root skill already on disk from every AI tool — in production we watched a team's entire shared root skill set (51 skills) disappear from all clients on the pull after manifest/projects.yaml first landed. Only skills with unpushed local edits survived, via the safety gate.

Root cause vs. prior design

This behavior was introduced deliberately in #911/#917 ("root = tag catalog: while the team uses roles or projects, root skills arrive only through a tag"), but it contradicts:

  • docs/usage-guide.md (namespace-override chapter): "Put shared content … at the root … everyone else keeps the shared one."
  • src/resource-namespaces.ts's own comment: deactivating projects must "scope down to role-only + shared resources".
  • The rules and agents resolvers, which both keep root items and let an active namespace replace them by name.
  • It is also a data-loss path: the cleanup treats "in the repo but not desired" as removable, so the empty desired set deletes every deployed root skill.

This PR unifies skills with the documented model: root skills are shared with every member; an active same-name namespace entry replaces the root one whole.

Semantic change to call out: tags subscribe no longer has an effect on root skills (they are always delivered); tags can still pull a skill out of a namespace the member has not activated. Docs updated accordingly in both languages.

Changes

  • src/resources/desired.ts
    • scanRoleAwareSkills: seed root skills/<name>/ directories (with SKILL.md) before scanning the active namespaces; resolveNamespacedItems then applies the existing "namespace replaces root" semantics.
    • buildRolePullContext: count root skill names as active, so a conflict run (which falls back to activeSkillNames) cannot prune a root skill that is byte-identical to an inactive-namespace copy.
  • src/pull.ts: drop the now-unreachable "root skills arrive only through a tag" hint and its tracking vars ([bug] pull removes root skills without a word when roles or projects take effect #911 wording is no longer true).
  • Tests: new regression cases in desired-skills.test.ts (empty-active-namespaces member still receives root skills; namespace replaces root with exact overrides report) and pull-namespace-override.test.ts (pull-level: root skills land, survive a second pull, namespace skill arrives after projects set, leaves after deactivation while root stays). Seven existing tests that encoded the old "root = tag catalog" semantics were rewritten to assert the documented semantics; no still-valid coverage was removed.
  • Docs: docs/usage-guide.md + .zh-CN.md, skill-data/setup/references/manage-admin.md synced to the unified semantics.

Testing

  • npx tsc --noEmit, npm run lint — clean.
  • npx vitest run desired-skills pull-namespace-override pull-tombstone pull-project-cleanup — 67/67.
  • Full npx vitest run — 8131 passed / 1 failed / 30 skipped; the single failure (uninstall.test.ts "removes the separate team-hook files of live bare worktrees") fails in setup because it needs git ≥ 2.42 for git worktree add --orphan and this machine has 2.39.5 — environment-only, unrelated.
  • E2E suites: npm run test:e2e -- multi-project roles-tags-pull namespaced-entries project-scoped-delivery — 12/12.

Real-CLI verification (before/after, built dist)

Sandbox: seed repo with skills/root-skill, skills/aivoice/proj-skill, manifest/projects.yaml (declares aivoice), teamai.yaml; bare remote via url.insteadOf; HOME=/tmp/e2e-rootfix/home.

Before (unfixed build — bug reproduced):

  • init --scope user --provider git → No resources to sync; root-skill absent from .claude/skills/.
  • After manually placing a root-skill copy, pull --force → Removed 1 skill(s) no longer delivered here: root-skill. While the team uses roles or projects, root skills arrive only through a tag… — the data-loss path.

After (this PR, same sandbox reset):

  • init → Synced 1 skills (1 new); ~/.claude/skills/root-skill/SKILL.md present, proj-skill absent.
  • Second pull --force → root skill kept, not cleaned.
  • teamai projects set aivoice + pull --force → Synced 2 skills (1 new, 1 updated); both proj-skill and root-skill on disk.
  • teamai projects set (deactivate) + pull --force → Removed 1 skill(s) no longer delivered here: proj-skill. (no stale tag hint); root-skill still on disk — namespace cleanup semantics intact.

…roles

For a member with no role and no active project on a team that has
manifest/projects.yaml, resolveDesiredSkills returned an empty set:
scanRoleAwareSkills only scanned namespace directories and never the
shared skills/<name>/ root. Root skills stopped deploying, and the
desired-union cleanup then pruned every root skill already on disk —
real teams lost their whole shared skill set on the next pull.

Root skills are shared per the usage guide and the rules/agents
resolvers: scanRoleAwareSkills now seeds the root SKILL.md directories
before the active namespaces, so an active same-name namespace entry
replaces the root one whole while every member keeps the shared copy.
buildRolePullContext also counts root names as active so a conflict run
cannot prune a root skill that is byte-identical to an inactive
namespace copy. The now-unreachable 'root skills arrive only through a
tag' hint (Tencent#911) is removed; tags can still pull a skill out of a
namespace the member has not activated.
@jeff-r2026 jeff-r2026 self-assigned this Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Findings

  • [P2 non-blocking] src/resources/desired.ts:223 — Root skills are now always added to directoryItems, but skippedByTags still counts every tagged root skill whose tag is unsubscribed. For example, a shared root skill tagged backend is delivered to a member subscribed only to frontend, while pull incorrectly reports it as “skipped by tags.” Count only tag-filtered items that are absent from the directory-delivered set.
  • [P2 non-blocking] docs/usage-guide.md:320 — The behavior change was not propagated to all affected documentation as required. docs/product-overview.md:70, docs/product-overview.zh-CN.md:70, and docs/designs/multi-project-management.md:235/:279 still state that root skills require tag subscriptions, directly contradicting the new behavior.

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

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Please resolve the conflicts.

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