Skip to content

feat(#375): multi-project management (P3) — member roster + projects commands + push --project - #444

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
jeff-r2026:feat/375-p3-members-cmd
Sep 9, 2026
Merged

feat(#375): multi-project management (P3) — member roster + projects commands + push --project#444
jeff-r2026 merged 1 commit into
Tencent:mainfrom
jeff-r2026:feat/375-p3-members-cmd

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

What & why

Implements issue #375 P3 (the final phase; P1+P2 landed in #426). Makes multi-project membership real and adds the low-frequency management commands.

Before this, members/<user>.yaml was a placeholder — init wrote it only when absent and recorded neither role nor project, so the team could not answer "who is on project X".

Changes

  • Member roster append-registration (members.ts, init.ts): init --project <id> now merges the active project(s) into members/<user>.yaml with append + dedupe semantics, on both the non-self and self (reports-branch) paths. Re-running init in another project directory adds that project to the roster instead of being a no-op. mergeMemberConfig is a pure helper (preserves registeredAt/displayName, dedupes projects, reports whether anything changed so we only write+push on a real change).
  • projects command family (projects-cmd.ts, index.ts) — mirrors teamai roles …:
    • projects list — defined projects + the ones active in this directory
    • projects set [ids...] — set active project(s) for this directory (overwrite; comma-separated or repeated; empty to clear; invalidates the pull cache so the next pull cleans up now-inactive resources)
    • projects members <id> — who is registered on a project
  • push --project <id> (push.ts, index.ts): resolves to the project's manifest skills namespace (not the raw id — the same Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375 P2 lesson) and reuses the existing --role landing logic; mutually exclusive with --role.
  • Docs: bilingual docs/usage-guide.md / .zh-CN.md — commands + membership behavior added to the multi-project section.

Test plan

tsc --noEmit clean · npm run build OK · 2765 unit tests pass (single-threaded; parallel mode has a pre-existing fs-fixture flakiness unrelated to this change). New mergeMemberConfig unit tests cover union/dedupe/role/empty cases.

Real-CLI end-to-end (TGit, repo created + verified + deleted)

Seeded a team repo with a projects manifest (hai, billing) and namespaced skills, then init --project in two business dirs:

Check Result
Member roster after init in both dirs → members/jeffyxu.yaml has projects: [hai, billing] (append)
projects list → both projects with their skills/knowledge/learnings namespaces
projects members hai → lists jeffyxu
projects set billing in the hai dir → active projects overwritten to billing
push --project billing → skill lands in skills/billing/my-new-skill (manifest-resolved namespace)

Closes #375 (final phase — P1+P2 in #426).

@jeff-r2026
jeff-r2026 force-pushed the feat/375-p3-members-cmd branch from fbba80e to 568750e Compare September 8, 2026 12:59
@jeff-r2026

Copy link
Copy Markdown
Collaborator Author

已修复 review 指出的 P1(清空项目会解除过滤)。commit 568750e

根因:buildRolePullContext() 在无角色且无激活项目时返回 null,而 null 语义是「不过滤,全量同步」——这本是给「从未使用项目/角色的旧仓库」的向后兼容行为。但它无法区分「明确清空了项目」和「旧配置默认」,于是 projects set(清空)后 pull 就退化成全量安装所有项目资源。

修复src/pull.ts buildRolePullContext):

  • 先加载 projects manifest:只要团队定义了项目(manifest 存在且非空),就认为该团队启用了项目分区。
  • 此时即使本目录无激活项目,也返回真实的(active 为空的)context 而非 null,让下游按空 active namespace 过滤,并把所有项目的 skills namespace 标记为 inactive 交给既有的清理路径 prune。
  • 仅当团队既无 roles manifest 也无 projects manifest 时,才保留 null(全量)的旧行为。

真机复现验证(TGit,完全按 review 步骤,建仓→验证→删仓):

  • 团队定义 alpha + billing,目录仅激活 alpha、未配角色。
  • pull.claude/skillsalpha-skill
  • teamai projects set(清空)。
  • pull.claude/skills 为空:billing 未安装(PASS),alpha 已清理(PASS)。

回归单测src/__tests__/pull-project-cleanup.test.ts —— 覆盖「清空 projects [] 返回非 null 且所有项目 skills 标记 inactive」「undefined projects 在团队有 manifest 时同样过滤」「无任何 manifest 的旧仓库仍返回 null(向后兼容)」。

门禁:tsc 0 · build OK · 单测 2769 全绿。

@jeff-r2026
jeff-r2026 force-pushed the feat/375-p3-members-cmd branch from 568750e to 94a5856 Compare September 9, 2026 08:09
@jeff-r2026

Copy link
Copy Markdown
Collaborator Author

已修复 review 指出的第二个 P1(清空项目后 pull 删除未推送的 skill 内容 → 数据丢失)。commit 94a5856

根因:pull 有两条 skill 清理路径,都盲目 remove() 整个部署目录,不检查本地是否有未推送的修改/新增:

  • cleanupInactiveNamespaceSkills(inactive 项目/角色 namespace 清理)
  • Step 3b(desired 集合之外、但仍在 repo 的 skill 清理)

清空项目后两条都命中该 skill → 目录连同本地改动 + unpublished.py 一起被删,且无 --force、无提示。

修复src/pull.ts):新增数据安全门 skillSafeToRemove(deployedDir, source),只有当部署副本与 team-repo 源一致时才删除;两条清理路径都改为先过此门,否则保留并告警

Kept skill "X": it has local changes or unpushed files not in the team repo. Push or back them up, then delete it manually.

一致性判断用 dirContentEqual(deployed, source, [CONTRIBUTORS])

  • 忽略部署时生成的 CONTRIBUTORSensureSkillFrontmatter 对已带完整 frontmatter 的源幂等,故干净部署的 skill 仍判为一致、可正常清理(不破坏 feat(#375): multi-project management (P3) — member roster + projects commands + push --project #444 的跨项目污染修复)。
  • 双向比较(文件集 + 内容都要相同),所以任何本地修改额外未推送文件都会使其不一致 → 保留。这正是子集比较做不到的(子集会忽略额外文件)。
  • 保守边界:源 skill 缺 frontmatter 时部署会注入、永不判等 → 保留而非删除(偏安全侧,用户可手动删)。已在注释说明。

真机复现验证(TGit,完全按 review 步骤,建仓→验证→删仓):

  1. 激活 alpha、pull → 部署 alpha-clean + alpha-dirty。
  2. 在 alpha-dirty 内改 SKILL.md + 新增 unpublished.py。
  3. teamai projects set(清空)+ 普通 teamai pull(无 --force)。
  • alpha-dirty 保留、unpublished.py 保留、本地改动保留(两条路径均打印 Kept 告警)。
  • alpha-clean(未改动)正常清理 —— 证明修复未破坏清理功能。

回归单测pull-tombstone.test.ts 新增「有本地改动 → 保留」「有未推送文件 → 保留」「源缺失无法验证 → 保留」;既有清理用例的 fixture 更新为真实部署内容(带 frontmatter)以反映 deployed==source。门禁:tsc 0 · build OK · 单测 2771 全绿。

另注:Step 3b 的盲删属 P1+P2 之前就存在的既有 tags 排除行为,本次一并纳入同一数据安全门。

@jeff-r2026
jeff-r2026 force-pushed the feat/375-p3-members-cmd branch from 94a5856 to f36b83b Compare September 9, 2026 08:32
@jeff-r2026

Copy link
Copy Markdown
Collaborator Author

已修复 review 指出的第三个 P1(skill 目录内的本地 .git / stash / 未推送历史被删)。commit f36b83b

根因:数据安全门 skillSafeToRemove 复用的 dirContentEqual 会跳过 .git(在 IGNORED_NAMES 里)。因此当 skill 目录是本地 git 仓库、且工作区已 git stash push -u 恢复到与团队源一致时,文件比较判为"相等" → 删除整个目录,连 .git(含 stash / 未推送提交 / reflog)一起丢失,无告警。文件一致 ≠ 目录内无未发布数据,正如 review 指出。

修复src/pull.ts skillSafeToRemove):在文件比较之前,先检查部署目录是否含本地版本控制元数据(.git / .hg / .svn)。只要存在就直接判定不安全、保留并告警 —— 其内容无法用工作区文件比较证明安全。采纳 review 建议。

真机复现验证(TGit,完全按 review 步骤,建仓→验证→删仓):

  1. 拉取 alpha 的 skill,在该 skill 目录 git init 并提交基线。
  2. 新增 unpublished.pygit stash push -u(工作区恢复为与团队源一致,只剩 SKILL.md)。
  3. teamai projects set(清空)+ 普通 teamai pull
  • 结果:skill 目录及其 .git 保留、stash(1 条)完整保留,两条清理路径均打印 Kept 告警。

回归单测pull-tombstone.test.ts 新增「skill 含本地 .git 即使文件与源一致也保留」。门禁:tsc 0 · build OK · 单测 2772 全绿。

至此本 PR 的清理逻辑对以下四类本地状态均安全:本地修改、未推送新增文件、本地 git 元数据(stash/历史)、以及无法验证来源时——全部保留 + 告警,只删与团队源逐字节一致(忽略部署生成的 CONTRIBUTORS)且无 VCS 元数据的干净副本。

@jeff-r2026
jeff-r2026 force-pushed the feat/375-p3-members-cmd branch from f36b83b to 5c85c0c Compare September 9, 2026 08:40
…ds + push --project

- members.ts: mergeMemberConfig (append+dedupe project membership)
- init.ts: member registration merges active projects into members/<user>.yaml
  (both non-self and self/reports-branch paths), no longer no-op on re-init
- projects-cmd.ts: projects list / set / members commands
- index.ts: register projects subcommand + push --project option
- push.ts: --project resolves to the project's manifest skills namespace
  (manifest-resolved, not raw id) and reuses the --role landing logic
- tests: mergeMemberConfig union/dedupe/role cases
@jeff-r2026
jeff-r2026 force-pushed the feat/375-p3-members-cmd branch from 5c85c0c to f11e2d1 Compare September 9, 2026 08:57
@jeff-r2026

Copy link
Copy Markdown
Collaborator Author

已修复 review 指出的嵌套 Git 仓库遗漏。commit f11e2d1

根因:上一轮的 hasLocalVcsMetadatapathExists(dir/.git) 检查 skill 根目录,而 dirContentEqual递归忽略任意深度的 .git。因此 scripts/.git 这样的嵌套 git 仓库:工作区文件与团队源一致、根目录无 .git → 两道检查都放行 → 整个 skill 连 scripts/.git 及其 stash 一起删。正如 review 指出,遍历不能沿用会忽略 .git 的扫描函数。

修复

  • 新增 hasVcsMetadataRecursive(dir)src/utils/fs.ts):独立的递归遍历,不复用忽略 .git 的 file-walk(那正是隐藏嵌套仓库的原因)。在任意深度匹配 .git/.hg/.svn目录或文件都算(submodule/worktree 的 .git 是文件);跳过 node_modules(依赖噪声,非用户工作)。
  • skillSafeToRemove 与 tombstone 清理路径都改用它替换只查根目录的版本。

真机复现验证(TGit,完全按 review 步骤,建仓→验证→删仓):

  1. skill 含 scripts/runner.py,在 scripts/git init
  2. 新增 extra.pygit stash push -u(工作区恢复为与团队源一致)。
  3. teamai projects set(清空)+ 普通 teamai pull
  • 结果:scripts/.git 保留、嵌套 stash(1 条)完整保留,两条清理路径均打印 Kept 告警。

回归单测

  • fs-compare.test.tshasVcsMetadataRecursive 覆盖 根 .git / 嵌套 scripts/.git / .git 文件形式(submodule) / .hg / .svn / node_modules 内跳过 / 目录不存在。
  • pull-tombstone.test.ts:新增「嵌套 scripts/.git 且文件一致 → 保留」。

门禁:tsc 0 · build OK · 单测 2780 全绿。

@jeff-r2026
jeff-r2026 merged commit 97fe5b7 into Tencent:main Sep 9, 2026
7 checks passed
@jeff-r2026
jeff-r2026 deleted the feat/375-p3-members-cmd branch September 9, 2026 09:07
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.

Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度)

1 participant