Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. See [standa

### ✨ Features

- Hooks, MCP servers and env variables can be scoped by logical project, the second membership axis they lacked. A `hooks/hooks.yaml` hook and an `mcp/mcp.yaml` server accept an optional `projects:` list beside `roles:`, and an `env/env.yaml` variable accepts both. An entry reaches a member when one of the projects its directory is bound to (`teamai projects set`) is listed; `projects: []` reaches nobody, and a directory bound to no project keeps receiving every entry, so nothing changes until a maintainer adds the key. The two axes compose as AND, the way `tools:` and `roles:` already do, so `roles: [frontend] projects: [checkout]` reaches frontend members of checkout rather than everyone on either. Rebinding with `teamai projects set` removes the previous project's entries on the next pull — for env that means the variable leaves `env.sh`, also on a pull that finds the team repo unchanged, so a machine upgrading from a CLI that ignored the keys drops a withheld variable without `--force`, and a refresh that cannot be written there is reported with the path and the way out rather than passing silently under `Already synced`; `teamai doctor` applies the same filter, so a variable correctly withheld is not reported as undelivered, while one that `env.sh` still exports after a rebind is reported until the next pull rewrites the file. An id that `manifest/projects.yaml` does not define produces one warning per pull, and so does a `projects:` key in a team with no projects manifest: the key still filters against the ids in the directory's `config.yaml`, but nothing can validate them. `teamai mcp list`, `teamai hooks list` and `teamai env list` show the restriction, and `pull` reports `Synced 1 of 3 env variable(s)` when scoping withheld some. This is what the keys exist to control: a team with five projects and three MCP servers each gave every member of a role fifteen server processes and fifteen tool lists in the context of every session (for [#668](https://github.com/Tencent/teamai-cli/issues/668)).

- `teamai doctor` now checks what landed for every resource, not only skills and docs. `Rules delivered to <tool>` and `Agents delivered to <tool>` ask the resource handler where an item lands — a rule's filename and content change per tool, an agent's destination comes from its render and its `targets:` — and compare a delivered rule with the bytes the handler renders for that tool, so a `.mdc` whose `globs` drifted from the team rule's `paths:` is reported rather than passing on the presence of its frontmatter keys. An agent is compared with the bytes its render produces, so a copy left behind by an older spec is reported rather than counted as delivered. `Every team agent reaches a tool` names an agent that renders for no installed tool, and is reported whenever a tool is installed to receive agents, including when no agent renders anywhere. Two tools do not read a rules directory and get a check each: `Team rules are active in opencode` fails when `opencode.json` stops listing the glob that makes the delivered `.md` files load at all, and `Team rules are inlined in Hermes SOUL.md` compares the managed block of `SOUL.md` with what the team rules inline to. `MCP servers delivered to <tool>` compares each server the team resolves for a tool with the entry in that tool's own config — the entry, not the name, since reconciliation leaves an entry teamai does not own alone, so an unrelated server under a team name holds the key while the team's definition never arrives — and names any the reconcile skipped with its reason, so an unresolved `${VAR}` is reported with the variable instead of being mentioned once during a pull and never again. An `mcp.yaml` that does not parse is reported as `Team MCP servers can be read` rather than read as a team shipping no MCP at all. `Env variables injected in shell profile` stops at the marker comment no longer: it checks that `env/env.yaml` parses and declares its variables under `variables:` (an explicit `variables: []` is an empty configuration and fails nothing), that each reached `env.sh` with the declared value — read back through the generator's own inverse, so a multiline value quoted across several lines is matched rather than reported stale — and that the injected block would actually load it. The two expensive registries, rules and agents, are built for `teamai doctor` only, so the checks at the end of a pull keep their budget (for [#624](https://github.com/Tencent/teamai-cli/issues/624)).
- A manual `teamai pull` ends by running the `teamai doctor` checks and printing each one that failed, with its fix. It prints nothing when they all pass, the exit code is unchanged, and the SessionStart hook path (`--silent`) and `--dry-run` run no checks, so session startup is untouched. Provider authentication checks are left to `teamai doctor`: the pull just used the provider. So is any check that pull already reported in its own words on that run — the queued-learnings warning is not immediately repeated as a check telling you to run the pull you just ran. A check the pull stayed silent about is still printed (for [#598](https://github.com/Tencent/teamai-cli/issues/598)).
- `teamai doctor` now checks what landed, not only the plumbing. `Skills delivered to <tool>` compares the skills your roles, tag subscriptions and exclusions resolve to against each installed tool's directory, reporting a skill that never arrived separately from one that arrived unreadable (`SKILL.md` missing, unparseable frontmatter, or a `name` that does not match the directory, which keeps the agent from discovering it). `Team docs delivered` does the same for the docs bundle against `sharing.docs.localDir`. `<tool> is installed` fails when `enabledAgents` lists a tool with no directory here, instead of skipping it silently, and reports an installed one as passing so `--json` carries an entry either way. Resolving a skill's destination without a team copy to compare against no longer warns about a Codex shared-directory conflict, so a read-only `doctor` stops reporting one for copies the pull treats as identical. The installed check asks the same resolver the sync uses, so OpenClaw is judged at its workspace directory rather than its tool root. `Team docs delivered` requires each expected document to be a readable file, not merely a name that exists. And a pull that found a scope locked by another process runs no checks at the end, since they would read a clone that process may have mid-write (for [#598](https://github.com/Tencent/teamai-cli/issues/598)).
Expand Down
15 changes: 15 additions & 0 deletions docs/designs/multi-project-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ experience) both need it, without affecting the single-project main path.

**Docs:** README (bilingual) + usage-guide (bilingual) per the CLAUDE.md sync rule.

**Extended by [#668](https://github.com/Tencent/teamai-cli/issues/668):** the three
per-item-scoped resource types this design did not cover. `hooks/hooks.yaml` and
`mcp/mcp.yaml` entries gain an optional `projects:` key beside their `roles:` one,
and `env/env.yaml` variables gain both — `src/membership.ts` resolves the two axes
together and ANDs them, so a delivery path cannot filter on one and forget the
other. Unlike resource namespaces, which take the role ∪ project union, a
per-item key is a restriction.

## Phasing

| Phase | Scope |
Expand Down Expand Up @@ -263,6 +271,13 @@ lone project; migrating existing flat learnings into a `shared/` subdirectory;
`teamai projects set --all` (the `all` selector is limited to `init --project` —
re-running `init --project all` already re-resolves the current manifest).

Also out of scope here, and delivered later by
[#668](https://github.com/Tencent/teamai-cli/issues/668): per-item project scoping
of hooks, MCP servers and env variables. Still unscoped on either axis after it:
`packages` (whose schema mixes an array with a nested object, so it is not the same
edit), `docs`, and `culture.md` — which suits a document defining how the whole
team works.

## End-to-end test plan (real CLI, per CLAUDE.md — type-check/unit tests don't count)

1. **No manifest → unchanged.** Repo without `projects.yaml`: `init`/`pull`/`recall`
Expand Down
6 changes: 3 additions & 3 deletions docs/product-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ Each resource is delivered to every agent:
| **Agents** | `agents/<name>.yaml`, `agents/<namespace>/<name>.yaml` | Root agents reach everyone; a namespace directory ships only to roles/projects that list it under `agents:` |
| **Culture** | `culture.md` | Team mission, values, and working principles — injected into each agent's CLAUDE.md / AGENTS.md so every session inherits them |
| **CLAUDE.md** | `claudemd/*.md` | |
| **Env** | `env/` | Shared team-level environment variables and switches; do not put secrets here |
| **Hooks** | `hooks/hooks.yaml` | Each hook may carry `roles:` to reach only members holding one of those roles |
| **MCP** | `mcp/mcp.yaml` | Each server may carry `roles:` to reach only members holding one of those roles |
| **Env** | `env/` | Shared team-level environment variables and switches; do not put secrets here. Each variable may carry `roles:` / `projects:` |
| **Hooks** | `hooks/hooks.yaml` | Each hook may carry `roles:` / `projects:` to reach only members holding one of those roles and directories bound to one of those projects |
| **MCP** | `mcp/mcp.yaml` | Each server may carry `roles:` / `projects:` to reach only members holding one of those roles and directories bound to one of those projects |
| **Packages** | `teamai.yaml` | Currently npm packages and Claude Code plugins only |
| **Models** | — | Not implemented for every provider yet |

Expand Down
6 changes: 3 additions & 3 deletions docs/product-overview.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ teamai push → 创建分支 + MR → reviewer 审批合并
| **Agents** | `agents/<name>.yaml`、`agents/<namespace>/<name>.yaml` | 根目录 agents 对所有人生效;namespace 子目录只同步给在 `agents:` 中列出它的角色/项目 |
| **Culture** | `culture.md` | 团队使命、价值观与协作准则——注入各 Agent 的 CLAUDE.md / AGENTS.md,成为每次会话的行事底色 |
| **CLAUDE.md** | `claudemd/*.md` | |
| **Env** | `env/` | 通用环境变量、团队级开关;不建议直接放密钥 |
| **Hooks** | `hooks/hooks.yaml` | 每条 hook 可加 `roles:`,只分发给持有这些角色的成员 |
| **MCP** | `mcp/mcp.yaml` | 每个 server 可加 `roles:`,只分发给持有这些角色的成员 |
| **Env** | `env/` | 通用环境变量、团队级开关;不建议直接放密钥。每个变量可加 `roles:` / `projects:` |
| **Hooks** | `hooks/hooks.yaml` | 每条 hook 可加 `roles:` / `projects:`,只分发给持有这些角色的成员、且绑定了这些项目的目录 |
| **MCP** | `mcp/mcp.yaml` | 每个 server 可加 `roles:` / `projects:`,只分发给持有这些角色的成员、且绑定了这些项目的目录 |
| **Packages** | `teamai.yaml` | 目前只支持 npm 包和 Claude 插件 |
| **Models** | — | 暂时没有对全部 provider 实现 |

Expand Down
34 changes: 34 additions & 0 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,27 @@ teamai env list
teamai push
```

Variables live in the team repo's `env/env.yaml`. `teamai env add` writes the first three fields; `roles` and `projects` are hand-edited, as they are for hooks and MCP servers:

```yaml
variables:
- key: API_ENDPOINT
value: https://api.example.com
description: Team API endpoint # optional
- key: CHECKOUT_DB_URL
value: https://checkout-db.internal
projects: [checkout] # optional; default is every directory
- key: DEPLOY_REGISTRY
value: registry.internal
roles: [devops] # optional; default is every member
```

`roles` and `projects` follow the same rule as on MCP servers and hooks: omitted reaches everyone, `[]` reaches nobody among members who use that axis, an axis the member has not configured filters nothing, and the two compose as **AND**. A variable that no longer matches is removed from `env.sh` on the next pull, even one that reports `Already synced` because the team repo has not moved, so changing role, running `teamai projects set` or upgrading the CLI takes it out of the member's shell without `--force`. Until that pull runs, `teamai doctor` reports a withheld variable that `env.sh` still exports, so the previous project's secrets are not left live in silence. `teamai env add` on an existing key keeps whatever `roles:`/`projects:` it already carries.

`pull` reports what reached this member, naming the declared total when the two differ (`Synced 1 of 3 env variable(s)`), so a variable that was scoped away is distinguishable from one that was lost.

Because the shell profile holds a single teamai block pointing at one `env.sh`, a machine that pulls in several project-scoped directories ends up with the last-pulled directory's variables in new shells. Each directory's own `env.sh` stays correct; it is the shell profile that can only point at one of them.

On `pull`, when `injectShellProfile` is enabled (default), the env block goes into `~/.zshrc` if `$SHELL` is zsh, otherwise `~/.bashrc` — except on Windows: `$SHELL` is normally unset there, and Git Bash starts as a *login* shell that never reads `.bashrc`, so teamai instead prefers an existing `~/.bash_profile`, then `~/.bash_login`, then `~/.profile`, falling back to `~/.bashrc` only when none of them exist (a zsh installed via MSYS2/Cygwin, which does set `$SHELL`, still resolves to `.zshrc`). This matches Git for Windows' own fallback in `/etc/profile.d/bash_profile.sh`, whose guard is `[ -e ~/.bashrc -a ! -e ~/.bash_profile -a ! -e ~/.bash_login -a ! -e ~/.profile ]` — it only synthesizes a `.bash_profile` that sources `.bashrc` in that same one case, which is why a stray `~/.profile` (even one that just sources something else, e.g. `~/.local/bin/env`) is enough to make `.bashrc` alone go unread. Override the target file with `sharing.env.shellProfilePath` in `teamai.yaml`.

This preference order only decides where a *first* pull writes. Every pull after that sticks to whichever candidate already carries this scope's block, rather than re-running the order — otherwise Git for Windows' own bootstrap would move the target out from under it: the same `/etc/profile.d/bash_profile.sh` guard above also means that first pull satisfies its condition (`.bashrc` now exists, nothing else does yet), so the next Git Bash login shell auto-generates a `~/.bash_profile` that sources it. Without sticking to `.bashrc`, the next pull would prefer that newly-created file and inject a second block there, leaving the original — still working, just loaded one hop further away — reported as a dead leftover.
Expand Down Expand Up @@ -777,12 +798,23 @@ servers:
requires: [npx] # skipped with a hint when npx is absent from PATH
tools: [claude, cursor] # optional; default is every capable tool
roles: [devops] # optional; default is every member
projects: [checkout] # optional; default is every directory
```

`requires` is resolved from `PATH`. On Windows a name also matches a `PATHEXT` suffix (`uvx` matches `uvx.exe` / `uvx.cmd`).

`roles` lists role ids from `manifest/roles.yaml`. A server ships to a member when one of their roles (`primaryRole` or `additionalRoles`) is listed; `roles: []` ships to nobody, the same way `tools: []` does. A member with no role configured receives every server, matching the unfiltered fallback skills and rules use. When a member changes role, servers that no longer match are removed on the next pull. Hand-added servers are never touched. An id that is not in `roles.yaml` produces one warning per pull. A teamai release older than this field ignores it and installs the server for everyone.

`projects` lists project ids from `manifest/projects.yaml` and follows the same rule on the other axis: a server ships to a directory when one of the projects it is bound to (`teamai projects set`) is listed; `projects: []` ships to nobody; a directory bound to no project receives every server. `teamai projects set` to another project removes the ones that no longer match on the next pull. An id that is not in `projects.yaml` produces one warning per pull, and so does a `projects:` key in a team that has no `projects.yaml` at all, where no id can be checked.

One caveat on the empty list, which applies to `roles: []` just as it always has. "Ships to nobody" holds among members who use that axis. A member who has not configured it at all is unfiltered and still receives the entry, because an unconfigured axis filters nothing. Use `tools: []` or remove the entry if you need it to reach no one at all.

A missing `projects.yaml` does not switch the key off. A directory's active projects come from its own `config.yaml`, so a directory bound to `billing` still filters out a `projects: [checkout]` server whether or not the manifest is there. What the manifest gives you is the ability to check the ids.

The two axes are independent and compose as **AND**: `roles: [frontend]` with `projects: [checkout]` reaches frontend members of checkout, not everyone on either. That is the same way `tools:` and `roles:` already compose, and deliberately not the union that role and project *resource namespaces* take — which answers the different question of which directories to sync.

This is the cost these keys exist to control: a team with five projects and three servers each gives every member of a role fifteen server processes and fifteen tool lists in the context of every session.

Where each tool's servers land:

| Tool | User scope | Project scope |
Expand Down Expand Up @@ -1396,6 +1428,7 @@ hooks:
timeout: 15
tools: [claude, cursor]
roles: [devops] # optional; default is every member
projects: [checkout] # optional; default is every directory

builtin:
disabled: [Hook dispatch post-tool-use TodoWrite]
Expand All @@ -1410,6 +1443,7 @@ builtin:
| `matcher` | Optional tool matcher |
| `tools` | Optional list of target tools (default = all tools that support hooks) |
| `roles` | Optional list of role ids from `manifest/roles.yaml` (default = every member; `[]` = nobody). Applied before the security gates below; a role change removes the previous role's hooks on the next pull. Ignored by older teamai releases. |
| `projects` | Optional list of project ids from `manifest/projects.yaml` (default = every directory; `[]` = nobody). Matches the projects this directory is bound to via `teamai projects set`; a rebind removes the previous project's hooks on the next pull. ANDs with `roles`. Ignored by older teamai releases. |
| `builtin.disabled` | List of disabled built-in hooks |
| `builtin.overrides` | Only the `timeout` of a built-in hook can be overridden |

Expand Down
Loading
Loading