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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. See [standa

### 💥 Breaking Changes

- An env variable, hook or MCP server with a key its schema does not know, such as a misspelled `role:` or a hand-added `notes:`, is no longer delivered to anyone: the key used to be dropped silently, so a misspelled restriction shipped the entry to every member. `teamai pull` and `teamai doctor` name the file, the entry and the key. `teamai env add`, `teamai env remove` and `teamai remove mcp` keep the key when they rewrite the file. A key that a later version adds to these entries is unknown to this one too, so an entry that uses it is not delivered to a member still on this version: upgrade every member before the team uses a new entry key, as for a new `resources:` key (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- `manifest/projects.yaml` and `manifest/roles.yaml` now reject a resource namespace that is not a single path segment, as a project id already had to be (the id keeps its own narrower ASCII rule). A namespace becomes a directory component (`skills/<namespace>/`, `agents/<namespace>/`, `learnings/<namespace>/`), so `../evil`, `a/b`, `C:evil`, a bare `..`, any name with a trailing `.` or space — which Win32 strips, making `.. ` arrive as `..` and `frontend.` as `frontend` — and a Windows device name such as `CON` or `COM1` under `resources:` no longer parse; the error names the offending entry. Nothing else is rejected: a namespace that is a plain directory name still parses, non-ASCII names and names with a space included. A manifest that fails to parse now reports the offending entry on one line (`Invalid projects manifest: projects.0.resources.skills.1: ...`) instead of dumping a raw validation object. Two namespaces of one resource type that differ only by case (`frontend`, `Frontend`) are rejected too, within a manifest and between the two, since they name one directory on Windows and macOS. A manifest that ships any of these — a device name, a trailing `.`, a case-only pair — parsed before and fails every pull now; rename the directory and the entry together.
- **Upgrade every member before a team declares a new axis.** `resources:` in `manifest/roles.yaml` and `manifest/projects.yaml` accepts `env`, `hooks`, `mcp`, `models` and `docs`, but teamai 0.25.0 and the 0.26.0 betas reject a `resources:` key they do not know, so a team that declares one breaks pull for every member still on those versions. From this version on, an unknown `resources:` key prints one warning naming the role or project and the key, and the scope syncs as if the key were absent; `teamai roles` and `teamai projects` keep the key when they save the manifest. `teamai roles|projects add/update --namespaces` never write the new keys, so nothing declares them until an admin does by hand (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
- Per-entry scoping of env variables, hooks and MCP servers gives way to namespace files (see Features). `projects:` on an `env/env.yaml` variable, a `hooks/hooks.yaml` hook or an `mcp/mcp.yaml` server, and `roles:` on an env variable, existed only in the 0.26.0 betas and are removed: such an entry now reaches nobody, and each pull warns with the namespace file to move it to, one per listed id, so a project-only value never falls through to the whole team. `roles:` on hooks and MCP servers, which 0.25.0 shipped, is deprecated: it keeps filtering for one more minor release as 0.25.0 did, a name repeated in one file under different `roles:` included, pull warns once per run and `teamai doctor` has a check, both naming every target file. There is no automatic migration; move each entry into the file the warning names and drop the key. A member with no role in a team with `roles.yaml` received every `roles:`-scoped hook and server; once they move into `hooks/<ns>/` or `mcp/<ns>/`, that member no longer does (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
Expand All @@ -31,6 +32,8 @@ All notable changes to this project will be documented in this file. See [standa

### 🐛 Bug Fixes

- A misspelled top-level key in `mcp/mcp.yaml` or `hooks/hooks.yaml` (`server:` for `servers:`, `hook:` for `hooks:`) no longer removes every installed team MCP server or hook: such a file read as empty. It now fails like a file that does not parse, so pull keeps what is installed, and pull and `teamai doctor` name the file, the keys found and the key expected. An extra top-level key beside `servers:` or `hooks:` is still ignored (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- The closing line of `teamai recall` output is in English (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- A broken team file no longer wipes or downgrades what a member has installed. An `mcp/mcp.yaml` or `hooks/hooks.yaml` that did not parse reconciled to an empty set and removed every team MCP server or hook from every tool, and two active namespaces defining one skill or agent aborted the pull for the whole scope, skipping rules, env, docs and cleanup. Now a file in the active set that does not parse or cannot be read, a name repeated inside one file, or one name in two active namespaces stops only that resource type for the run: env keeps `env.sh`, hooks and MCP keep their entries (the built-in hooks, with the session-start pull, are still installed where missing: with the root hooks file's `builtin:` overrides when it parses, and otherwise with their defaults only in a tool that has none yet; `teamai init` says the team hooks were not installed), model profiles leave switched agents alone, skills and agents keep what is installed, and every other type still syncs. The warning names the file or both files and the fix, and for env, hooks, MCP and models is also written to `~/.teamai/debug.log` for session-start pulls (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
- In single-repo (`kind: self`) mode, every checkout of the business repo now publishes learnings and reports. The `teamai-learnings` and `teamai-reports` checkouts and the queue of unpublished learnings lived in each checkout's own `.teamai/`, and git checks a branch out in one worktree only, so the first checkout to create them locked every other one out (`'teamai-learnings' is already used by worktree`), and a learning queued in a linked worktree was deleted with it by a plain `git worktree remove`. They now live in the project partition that every checkout shares, and the search index is kept per checkout, so `recall` no longer serves another checkout's index with paths into it. On upgrade, `init`, `pull`, `push`, `contribute` and `import --from-mr` migrate a checkout's old install, moving its queue into the partition without overwriting anything (or into `pending-learnings.self` when another checkout has since switched the project to another kind; an old git-mode install beside the knowledge of a switch to single-repo mode goes to `.teamai.bak`, its queue to `pending-learnings.git`); `contribute` and `import --from-mr` stop, saving nothing, while that data cannot move out of the checkout; and the first command that needs a side-branch checkout removes the old one from `.teamai/`; one with uncommitted changes is kept, a warning names it and says what to do, and `recall maintenance` and `recall promote` stop until then, while another command holds the learnings or reports lock, and when a checkout cannot be created, naming the cause. A git-mode install of the same project uses the same partition paths, so a checkout that belongs to the other repository is refused, never reused or removed, with the command that clears it, and so is one whose repository was moved or deleted; its votes are not counted, `init` deletes the search indexes built for the old repository, learnings the old install still had queued are set aside in `pending-learnings.<old kind>` rather than published by the new one (and in `pending-learnings.<kind>-<repo>` when `init` points an install at another team repository of the same kind, which on `main` published them to the new one; the same repository written another way keeps them, [#823](https://github.com/Tencent/teamai-cli/issues/823) item 13), and `uninstall` lists every unpublished queue before it asks. (for [#808](https://github.com/Tencent/teamai-cli/issues/808)).
- A scope's usage file no longer grows without bound where it is never reported: an http source, a team with `usageReport: false`, or a remote that rejects every push. `teamai pull` keeps its newest 5,000 events, so `teamai stats` still shows recent usage there, the local file being its only source. In a reporting scope whose report does not complete while it holds more than 5,000 events, the dropped events were never reported. The cap runs after the report has removed the events it sent, so it cannot shift that removal onto events not yet reported, and a file at or below the cap is not rewritten. Hook appends, the report's truncate and the cap now share one lock beside the usage file, and a rewrite goes through a temp file that keeps the file's mode, so a rewrite no longer loses an event recorded while it runs, nor the file on a kill or a full disk. A hook that cannot take the lock within ~250 ms records its event in a `*.pending-<id>.jsonl` file beside it, which the next lock holder appends; a rewrite that cannot take it within ~5 s leaves the file as it is. A pending file gets no wider mode than the usage file (owner-only while there is none), and an in-workspace `.teamai/.gitignore` ignores the lock, a rewrite's temp copy and the pending files; `pull` and `push` add those entries to an existing single-repo one, and the first pending file or rewrite adds them to an existing project-scope one (for [#788](https://github.com/Tencent/teamai-cli/issues/788)).
Expand Down
26 changes: 17 additions & 9 deletions docs/designs/multi-project-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,9 @@ resolved env set.
Skills keep one difference: in role/project mode the root `skills/` stays the tag
catalog and is not delivered by default. A root skill that arrives through a
subscribed tag is replaced by an active namespace skill of the same name, and
among tag matches the root skill wins over one in an inactive namespace. Installing
among tag matches the root skill wins over one in an inactive namespace. A tagged
skill that exists only in an inactive namespace still reaches a subscribed member:
tags cross namespaces by design (#337). Installing
a skill removes the files that another team version of that skill (root or any
namespace) has and the new one lacks, when they match that version byte for
byte, so switching versions leaves no team file behind; a file the member added
Expand Down Expand Up @@ -408,7 +410,20 @@ and pull warns with the namespace file to move it to, one per listed id.
`roles:` on hooks and MCP shipped in 0.25.0 and keeps filtering for one more
minor release; pull warns once per run and `doctor` has an informational check,
both naming every target file. Model profiles are strict, so a per-entry key
fails the file. There is no automatic migration.
fails the file. An env, hook or MCP entry with any other key its schema does not
know, such as a mistyped `role:`, reaches nobody too, and pull and `doctor` name
the file, the entry and the key (#822); `env add`, `env remove` and `remove mcp`
keep such a key when they rewrite the file. A key that a later version adds is
unknown to this one as well, so an entry that uses it is not delivered to a member
still on this version: every member has to upgrade before the team uses a new
entry key, as for a new `resources:` key. A hooks or MCP file with none of its
top-level keys (`server:` for `servers:`, `hook:` for `hooks:`) used to read as
empty and remove every installed server or hook; it now fails like a file that
does not parse, naming the keys it found, as `env.yaml` has since #662. An extra
top-level key beside a known one is still ignored. `pull --dry-run` resolves the hooks
and MCP entries and reports their warnings (an unknown id, a per-entry key, a file
that does not parse) without writing, so a maintainer can see them before a real
pull applies them. There is no automatic migration.

### Push and commands

Expand All @@ -434,15 +449,8 @@ entry comes from.

### Known gaps

- The tag channel matches skills by name across every namespace, so a tagged
skill that exists only in an inactive namespace still reaches the member.
- No pull protects a local edit from being overwritten, override transitions
included.
- A mistyped per-entry key (`role:`) is stripped by the schema and the entry
reaches everyone. `pull --dry-run` resolves the hooks and MCP entries and
reports their warnings (an unknown id, a deprecated per-entry `roles:`, a file
that does not parse) without writing, so a maintainer can see them before a
real pull applies them.

## Backward compatibility

Expand Down
11 changes: 11 additions & 0 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,17 @@ The per-entry keys these files replace:
There is no automatic migration: move each entry into the namespace file the
warning names, and drop the key.

An entry with any other key its schema does not know, such as a mistyped `role:`,
reaches nobody as well, and pull and `teamai doctor` name the file, the entry and
the key. Correct the key or remove it. A key that a later teamai version adds is
unknown to an older one too, so upgrade every member before the team uses a new
entry key.

A hooks or MCP file that has none of its top-level keys, such as `server:` for
`servers:`, is treated like a file that does not parse: pull keeps the installed
servers or hooks, and pull and `teamai doctor` name the file, the keys found and
the key expected. An extra top-level key next to `servers:` or `hooks:` is ignored.

### Env (environment variables)

```bash
Expand Down
9 changes: 9 additions & 0 deletions docs/usage-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,15 @@ projects:

没有自动迁移:把每个条目移到警告给出的 namespace 文件中,并删掉该 key。

条目若带有其 schema 不认识的其他 key(例如拼错的 `role:`),同样不会下发给任何人;
pull 与 `teamai doctor` 会指出文件、条目和该 key。请改正或删除这个 key。
较新版本 teamai 新增的 key 对旧版本同样是未知 key,因此团队使用新的条目 key 之前,
请先让所有成员升级。

hooks 或 MCP 文件若没有任何一个应有的顶层 key(例如把 `servers:` 写成 `server:`),
按无法解析的文件处理:pull 保留已安装的 server 或 hook,pull 与 `teamai doctor`
会指出文件、实际找到的 key 和应有的 key。`servers:` 或 `hooks:` 旁多出的顶层 key 会被忽略。

### Env(环境变量)

```bash
Expand Down
7 changes: 6 additions & 1 deletion skill-data/setup/references/manage-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,15 @@ and push it with git. `teamai doctor` lists each override.

- A name twice in one file, in two active namespaces, or an active file that does
not parse: that type is not applied for affected members and their installed
state is kept. Fix the file the warning names.
state is kept. Fix the file the warning names. A hooks or MCP file with none of
its top-level keys (`server:` for `servers:`) counts as one that does not parse.
- Per-entry `projects:` (and `roles:` on env) no longer works: such an entry reaches
nobody. `roles:` on hooks and MCP still filters for one more minor release. Pull
and `teamai doctor` name the namespace file each entry belongs in; move it there.
- An env, hook or MCP entry with a key its schema does not know (a mistyped `role:`)
also reaches nobody. Pull and `teamai doctor` name the file, entry and key; correct
the key or remove it. A key a later teamai version adds is unknown to an older one,
so upgrade every member before the team uses a new entry key.
- Team model profiles work the same way: `models/<ns>/models.yaml`, declared under
`resources.models`, replaces the root profile with the same `id` for members who
have `<ns>` active. A member's API key is bound to the profile's gateway origin:
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/doctor-entry-namespaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ describe('doctor — env, hooks and MCP namespaces', () => {
expect(check.fix).toContain('env/env.yaml: variable "A" is scoped with per-entry `projects:`');
});

it('names an entry with a key its schema does not know in the same check (#822)', async () => {
await fse.outputFile(path.join(repoPath, 'mcp', 'mcp.yaml'), [
'servers:',
' - { name: db, transport: http, url: https://a.example.com, role: [frontend] }',
'',
].join('\n'));

const [check] = await buildEntryScopeKeyCheck(ctx());
if (!check) throw new Error('expected the scope-key check');
expect(check.fix).toContain('mcp/mcp.yaml: server "db" has unknown key `role:`, so this entry is not delivered');
});

it('adds no check when no entry carries a per-entry key', async () => {
await fse.outputFile(path.join(repoPath, 'hooks', 'hooks.yaml'), 'hooks:\n - { id: lint, description: x, event: Stop, command: echo }\n');
expect(await buildEntryScopeKeyCheck(ctx())).toEqual([]);
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/e2e/project-scoped-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,28 @@ describe('project-scoped hooks, MCP servers and env variables via the real CLI (
expect(pull.output).toContain('Move it to mcp/billing/mcp.yaml and drop the key.');
expect(readClaudeMcp()).not.toContain('legacy-api');
}, 60_000);

// A typo of a scoping key (#822) must not widen who gets the server.
it('installs no server that carries an unknown key, and warns naming the file, server and key', async () => {
const teamRepo = path.join(projectRoot, '.teamai', 'team-repo');
fs.writeFileSync(path.join(teamRepo, 'mcp', 'mcp.yaml'), [
'servers:',
' - name: typo-api',
' transport: http',
' url: https://typo.example.com/mcp',
' role: [frontend]',
' - name: shared-api',
' transport: http',
' url: https://shared.example.com/mcp',
'',
].join('\n'));

const pull = await runCLI(['pull', '--force'], projectRoot, home);
expect(pull.code, pull.output).toBe(0);
expect(pull.output).toMatch(/mcp\/mcp\.yaml: server "typo-api".*\brole\b/);
expect(readClaudeMcp()).not.toContain('typo-api');
expect(readClaudeMcp()).toContain('shared-api');
}, 60_000);
});

// Codex has no project-scope MCP location (no `mcpProject` in toolPaths), so its
Expand Down
18 changes: 18 additions & 0 deletions src/__tests__/env-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ scope: 'user',
});
});

// A variable with a misspelled `roles:` reaches nobody (#822); a rewrite
// that drops the key would deliver it to the whole team.
it('preserves a key env does not know on a variable it updates', async () => {
await fse.writeFile(
path.join(repoPath, 'env', 'env.yaml'),
YAML.stringify({ variables: [{ key: 'DB_URL', value: 'old', role: ['frontend'] }] }),
);

await envAdd('DB_URL', 'new', {});

const parsed = YAML.parse(await fse.readFile(path.join(repoPath, 'env', 'env.yaml'), 'utf-8'));
expect(parsed.variables).toEqual([{ key: 'DB_URL', value: 'new', role: ['frontend'] }]);
expect(log.warn).toHaveBeenCalledWith(
'env/env.yaml: variable "DB_URL" has unknown key `role:`, so pull does not deliver it. '
+ 'Correct the key or remove it in env/env.yaml.',
);
});

it('preserves the scope of other variables when adding a new one', async () => {
await fse.writeFile(
path.join(repoPath, 'env', 'env.yaml'),
Expand Down
Loading
Loading