Tencent / Tencent/teamai-cli

[feat] Scope hooks, MCP servers and env variables by project: a project's entries reach every member of the role

Open
#668 0 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.