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.
Problem
TeamAI resolves delivery on two membership axes. Roles namespace
knowledge,skillsandagents(src/roles.ts:11-20). Projects namespace those pluslearnings(src/projects.ts:12-21). Skills, rules, agents and claudemd resolve on both axes. Learnings resolve on projects alone, becauseresolveRoleResourceNamespacesignores the role manifest'slearningskey at runtime (src/roles.ts:17-20).The three resource types that carry per-item scoping do not have both axes:
tools:,roles:(src/resources/hooks.ts:16-33)tools:,roles:(src/resources/mcp.ts:15-35){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 pullwrites an env variable into the member's shell profile, between theTEAMAI_ENV_STARTandTEAMAI_ENV_ENDmarkers.A team with five projects and three MCP servers each gives a member holding role
frontfifteen 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
matchesRolesalready implements (src/roles.ts:195-198):The last line is the part to get right. A member holds several roles through
primaryRoleplusadditionalRoles(src/roles.ts:185-188), and is bound to several projects throughprojects: string[](src/types.ts:461-468). Soprojects:matches on an intersection, the same wayroles:does, and not on equality with a single active project.One half of the pair is missing.
activeRoleIdsreadsprimaryRoleandadditionalRoles, and nothing readslocalConfig.projects:warnUnknownRoleIdswarns once per pull for a role id thatroles.yamldoes not define, so a typo does not ship an entry to nobody in silence (src/roles.ts). The project side needs the same warning againstmanifest/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.
EnvVariableSchemais 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
packageshas 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 pullcopiesdocswhole (src/pull.ts:842-852). It readsculture.mdunconditionally (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.