[feat] Scope hooks, MCP servers and env variables by project: a project's entries reach every member of the role
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4.8k
- Forks
- 342
- Avg merge
- 13h 48m
- Merged PRs (30d)
- 211
Description
Problem
TeamAI resolves delivery on two membership axes. Roles namespace knowledge, skills and agents (src/roles.ts:11-20). Projects namespace those plus learnings (src/projects.ts:12-21). Skills, rules, agents and claudemd resolve on both axes. Learnings resolve on projects alone, because resolveRoleResourceNamespaces ignores the role manifest's learnings key at runtime (src/roles.ts:17-20).
The three resource types that carry per-item scoping do not have both axes:
| Resource | Role | Project | Per-item keys today |
|---|---|---|---|
| hooks | yes | no | tools:, roles: (src/resources/hooks.ts:16-33) |
| mcp | yes | no | tools:, roles: (src/resources/mcp.ts:15-35) |
| env | no | no | none. {key, value, description?} (src/resources/env.ts:13-17) |
These three are the types whose delivery costs something on every session rather than once. An MCP server starts a process and adds its tools to the tool list of every agent session. teamai pull writes an env variable into the member's shell profile, between the TEAMAI_ENV_START and TEAMAI_ENV_END markers.
A team with five projects and three MCP servers each gives a member holding role front fifteen servers: fifteen process starts, and fifteen tool lists in the context of every session. The cost grows with the number of projects a team runs.
A member keeps one role while moving across several projects, so a role cannot express which project an entry belongs to. Four resource types already carry both axes for that reason.
Proposed Solution
One optional key on hooks and MCP servers, beside the key it mirrors:
tools: z.array(z.string()).optional(),
roles: z.array(z.string()).optional(),
+projects: z.array(z.string()).optional(),
Both keys on env variables, whose schema is the same array of objects:
const EnvVariableSchema = z.object({
key: z.string(),
value: z.string(),
description: z.string().optional(),
+ roles: z.array(z.string()).optional(),
+ projects: z.array(z.string()).optional(),
});
Matching follows the rule matchesRoles already implements (src/roles.ts:195-198):
matches(entryKeys, memberKeys)
entryKeys omitted -> everyone
entryKeys empty -> nobody
otherwise -> entryKeys and memberKeys share at least one id
The last line is the part to get right. A member holds several roles through primaryRole plus additionalRoles (src/roles.ts:185-188), and is bound to several projects through projects: string[] (src/types.ts:461-468). So projects: matches on an intersection, the same way roles: does, and not on equality with a single active project.
One half of the pair is missing. activeRoleIds reads primaryRole and additionalRoles, and nothing reads localConfig.projects:
hook / mcp / env delivery
matchesRoles(entry.roles, activeRoleIds(localConfig))
activeRoleIds src/roles.ts:185-188 exists
matchesProjects(entry.projects, ???)
no reader for localConfig.projects missing
warnUnknownRoleIds warns once per pull for a role id that roles.yaml does not define, so a typo does not ship an entry to nobody in silence (src/roles.ts). The project side needs the same warning against manifest/projects.yaml.
Alternatives Considered
Model projects as roles. A member keeps one role while moving across projects, so this collapses two axes the manifest already separates. It would also leave the four resource types that carry both axes with a meaning the other three do not share.
Use the existing tools: key. It restricts by harness, not by membership. A project's server still reaches every member who has that harness installed.
Change hooks and MCP only, and leave env for later. EnvVariableSchema is the same array of objects, so it is the same edit. Env is also the one of the three that lands in the member's shell profile rather than in a tool's config file.
Additional Context
packages has no axis either (src/pkg/types.ts:45-48), and its npm packages and Claude plugins install on every member's machine. Its schema mixes an array with a nested object, so it is not the same edit, and this proposal leaves it out.
Two more types have no axis, at lower cost. teamai pull copies docs whole (src/pull.ts:842-852). It reads culture.md unconditionally (src/pull.ts:1316-1320), which suits a document that defines how the whole team works.
Tags are a third axis. They reach skills (src/pull.ts:363) and rules (src/pull.ts:785) only.
Read from source at main (e10bbcb). Installed CLI 0.24.0, Node v22.22.2, macOS 26.5.2.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read the resource schemas in src/resources/hooks.ts, src/resources/mcp.ts, and src/resources/env.ts, then trace delivery and role matching through src/roles.ts and src/pull.ts. Check how member projects are represented in src/types.ts and how manifests are loaded before defining project matching and unknown-project warnings. Done means hooks, MCP servers, and environment variables are filtered by both role and project without changing existing omitted-key behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100