Repository navigation
Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375
Description
背景
一个团队仓库往往同时承载多个项目,但现有的资源分发只有两个旋钮:
| 机制 | 载体 | 粒度 | 谁控制 |
|---|---|---|---|
| roles | manifest/roles.yaml |
namespace(目录级) | 团队 admin |
| tags | tags.yaml |
单个 skill / rule | 团队打标 + 个人订阅 |
多项目场景下有三个具体问题:
P1 — 语义被压成一维。 role 同时承担「职能」和「资源包」(src/roles.ts:130 resolveRoleResourceNamespaces())。要表达「HAI 的开发」「HAI 的 PM」「计费的开发」,就得为每个「项目 × 职能」组合定义一个 role。N 个项目 × M 个职能 = N×M 个 role,manifest 迅速不可维护。
P2 — learnings 完全不隔离(最痛的一条)。 src/roles.ts:13 明确注明 learnings namespace 被忽略;teamai contribute 一律写入扁平的 learnings/(src/contribute.ts:184);索引构建用的是非递归的 collectFlatMdEntries(src/utils/search-index.ts:549),子目录根本扫不到。结果:项目 A 的经验会进入项目 B 成员的 teamai recall 结果,随项目数增长召回信噪比线性恶化。
注意 project scope 隔离不了这个 —— 两个目录 clone 的是同一个团队仓库,learnings/ 依然是扁平的一坨。
P3 — 成员注册只是占位。 members/<user>.yaml 的 schema(src/types.ts:262)有 role? 字段,但实际写入时连 role 都不写(src/init.ts:1142、src/init.ts:824)。团队侧无法回答「谁在项目 X 里」。
tags 为什么不够。 tags 是个人本地订阅(存在 config.yaml 的 subscribedTags),团队侧不可见、不可查询;且只覆盖 skill/rule 粒度,不管 learnings、claudemd、docs,也没有「成员归属」语义。它适合表达「我对某个主题感兴趣」,不适合表达「我是这个项目的人」。
方案:project 作为与 role 正交的第二维度
新增 manifest/projects.yaml,与 roles.yaml 平级、互不引用:
version: 1
projects:
- id: hai-inference
name: HAI 推理平台
resources:
knowledge: [hai-inference]
skills: [hai-inference]
learnings: [hai-inference] # 让 learnings namespace 真正生效目录布局复用现有 namespace 约定,只新增 learnings 一层:
team-repo/
manifest/roles.yaml projects.yaml
skills/ common/ hai-inference/ billing/
claudemd/ common/ hai-inference/ billing/
learnings/
团队通用经验-2026-03-15.md # 根目录 = 全团队共享(现状不变)
hai-inference/ # 新增:项目私有经验
billing/
关键约定:learnings/ 根目录下的 .md 永远对所有人可见。 这既是向后兼容的支点(今天所有 learnings 都在根目录,零迁移),也是跨项目通用经验的天然去处。
命名空间解析:
activeNamespaces = resolveRole(primaryRole, additionalRoles)
∪ resolveProjects(activeProjects)
roles.yaml 的 learnings 字段继续被忽略 —— learnings namespace 只由 projects 提供,否则又回到 P1 的语义混淆。
入口:跟着工作目录走,不引入 join/leave
项目身份由工作目录决定,与现有 --role 完全同一个模式:
cd ~/work/hai-inference && teamai init <team-repo> --project hai-inference
cd ~/work/billing && teamai init <team-repo> --project billing早期草案里有 teamai projects join/leave,是从 tags subscribe/unsubscribe 类比来的。这个类比不成立,记录在此以免重新提出:
| tags | projects | |
|---|---|---|
| 表达什么 | 「我对某个主题感兴趣」 | 「这个目录属于哪个项目」 |
| 有没有外部依据 | 没有,纯个人偏好 | 有,就是 cwd |
| 是否需要显式开关 | 需要 | 不需要 —— init 时已经知道了 |
成员登记因此变成 init 的副作用,不是独立命令。两处 projects[] 语义刻意不同:
| 位置 | 内容 | 语义 |
|---|---|---|
<project>/.teamai/config.yaml |
本目录激活的项目 | 覆盖 |
仓库 members/<user>.yaml |
我参与过的所有项目 | 追加去重 |
在两个目录各 init 一次,名册上自然是两个项目,而每个目录只同步自己那份。实现上要改 src/init.ts:1140 的 isNewMember 分支(现在是「文件不存在才写」)。
保留 teamai projects set/list/members 作为低频的事后修正与查询,对标已有的 teamai roles set 之于 init --role。
config.yaml 里的 projects 仍是数组 —— monorepo(一个仓库多个子项目)和跨项目职能(平台/基础设施的人要看多边经验)都需要,但不影响单项目主路径。
与既有 issue 的关系
依赖 #374。 本方案的主路径成立的前提是「cwd → 项目根」能被正确解析,而 #374 已查明 detectProjectConfig() 今天只查 cwd 当层、不向上遍历,且在 git worktree 里必然探测失败并静默回落 user scope。也就是说,在子目录或 worktree 中本方案的项目识别会跟着一起失效。建议 #374 先落地。
同时要注意两处「项目」是不同概念,实现时别混用:
| #374 的 project | 本方案的 project | |
|---|---|---|
| 是什么 | 一个工作目录(路径 slug) | 团队定义的逻辑项目(manifest id) |
| 由谁决定 | 路径,纯函数 path → slug |
admin 写在 manifest/projects.yaml |
| 解决什么 | 机器本地数据存在哪 | 团队知识分发给谁 |
| 举例 | -Users-x-work-hai |
hai-inference |
两者正交:#374 落地后,本方案的字段就住在 ~/.teamai/projects/<path-slug>/config.yaml 的 projects: [<logical-id>] 里,一个路径 slug 对应 0..N 个逻辑项目。#374 顺带也解决了「每个目录一份 team-repo clone」的成本问题。
与 #341(Go 管理后端)对齐。 #341 的模型是 Organization → Team → Project + 逐层资源覆盖 + 项目成员角色管理,project 语义与本方案一致,差别只在载体(projects.yaml vs 后端数据库)和治理方式(Git PR vs 审核状态机)。建议把 projects.yaml 作为该模型的声明式表示 —— 后端将来接管时它就是天然的导入/导出格式。本方案刻意不引入 Organization / Team 层级:一个团队仓库天然对应一个 Team,多出来的层级在 Git 模式下没有承载物。
向后兼容
| 场景 | 行为 |
|---|---|
无 manifest/projects.yaml |
与今天完全一致,所有 project 代码路径短路返回 |
| 有 manifest、目录未激活任何项目 | role namespaces + learnings/ 根目录 |
老 members/<user>.yaml / config.yaml 无 projects |
解析为 [],不报错 |
现存扁平 learnings/*.md |
全部留在根目录 = 全员共享,零迁移 |
影响面
新增:src/projects.ts、src/projects-cmd.ts、对应单测。
修改:types.ts(两个 schema 加 projects)、roles.ts(ResourceNamespaces 加 learnings 键)、pull.ts(namespace 合并 + learnings 同步与清理)、push.ts(--project 落点)、contribute.ts(落点优先级)、utils/search-index.ts(namespace 感知的 learnings 收集器)、init.ts(--project + member 追加登记)、bootstrap.ts、members.ts、index.ts。
文档:README 双语 + usage-guide 双语。
分阶段
| 阶段 | 范围 |
|---|---|
| P1 | projects.ts + manifest + LocalConfig + pull 的 skills/rules/claudemd 过滤 |
| P2 | learnings namespace + 索引收集器 + contribute 落点 + pull 清理 |
| P3 | member 追加登记与查询 + projects set/members + push --project |
| P4 | 双语文档同步 |
建议 P1 与 P2 合成一个 PR —— 两者共享 namespace 解析改动,拆开会产生一个别扭的中间态:项目已隔离但经验仍然串台,而这恰恰是最痛的那一半。P3 单独一个 PR。
Open Questions
Q1 — manifest 里只有一个项目时要不要自动激活? roles 在只有 1 个时会自动选中(src/init.ts:85)。projects 当前设计为不自动:role 是「你必须有一个」,project 是「你可以一个都不属于」—— 一个仓库定义了项目不代表每个成员都在某个项目里(做基础设施的可能只需要 common),自动激活会让这类人收到项目私有 learnings,又回到 P2。若实际使用中发现绝大多数团队就是「一仓一主项目」,这条可以放开,改动很小。
Q2 — learnings/ 根目录该不该有个显式的 shared/ 目录? 当前设计用「根目录 = 共享」,好处是零迁移,代价是共享与分区在同一层,目录列表看起来不齐整。
完整设计文档(含数据模型、边界情况、10 步端到端测试计划)在 docs/designs/multi-project-management.md,随实现 PR 一起提交。
Activity
feat(Tencent#375): multi-project P1+P2 — project dimension, learnings…
Merge pull request #426 from jeff-r2026/feat/375-multi-project-p1p2
feat(Tencent#375): multi-project P3 — member roster + projects comman…
feat(Tencent#375): multi-project P3 — member roster + projects comman…
feat(Tencent#375): multi-project P3 — member roster + projects comman…
feat(Tencent#375): multi-project P3 — member roster + projects comman…
feat(Tencent#375): multi-project P3 — member roster + projects comman…
Merge pull request #444 from jeff-r2026/feat/375-p3-members-cmd
test(e2e): add real-CLI e2e for data-layout migration (#374) and mult…