Skip to content

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

Closed

Description

@jeff-r2026

背景

一个团队仓库往往同时承载多个项目,但现有的资源分发只有两个旋钮:

机制 载体 粒度 谁控制
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

  1. added 2 commits that reference this issue on Sep 7, 2026
  2. added 7 commits that reference this issue on Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions