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

### 🐛 Bug Fixes

- `teamai pull` keeps a skill, rule or agent you changed since teamai delivered it instead of overwriting it, and names it: `Kept <path>: you changed it since teamai delivered it`, with a warning when the team version has changed since, which `teamai push` repeats for that copy, since the SessionStart pull is silent. Pull records the sha256 of what it writes at each path in the checkout's record, and the pre-push sync records its writes too, so a copy counts as changed only against that record; a skill counts as one copy, and files only you added do not count. The copies of other tools still update. `--force` keeps these copies, `--dry-run` prints `Would keep <path>`, and a copy of an item the team removed stays when you changed it. To take the team version, delete your copy and run `teamai pull --force`. There is no record before the first full pull on this version, or in a new worktree, so that pull overwrites as before. A file you added to a skill at a path another team version of it has is no longer named on every pull, and `teamai doctor` no longer fails a rules or agents check, or points at `teamai pull --force`, for a kept copy: it lists one as `changed by you (kept by pull)` beside any other problem (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- On macOS, the git commands behind pull, push, reports and learnings publishing no longer wait on a PATH search each time they run. teamai spawned `git` by name, and on macOS that lookup costs a few milliseconds for each PATH entry ahead of git's directory: 30-67 ms per call with a typical PATH, while git itself takes about 8 ms. teamai now runs the `git` that lookup would pick by its absolute path, found once and again whenever PATH changes or that `git` is gone or no longer executable, and only once a `git --version` by that path starts. On macOS 26 an up-to-date `teamai pull` drops from 1.3 s to 0.4 s with a 42-entry PATH, and from 2.0 s to 0.4 s with the PATH an npm script gives; macOS 27 no longer shows the lookup cost. Windows, a PATH with an empty or relative entry, a first `git` on PATH that does not start (a missing interpreter, no execute permission), and the few one-off git calls outside these paths (provider clone, version and user probes) keep the bare-name spawn (for [#868](https://github.com/Tencent/teamai-cli/issues/868)).
- On Node 24, `teamai codebase --extract`, and the `teamai import` and CI extract paths that run it, no longer abort with `Fatal process out of memory: Zone` on a repository with `.swift` files. V8's optimizing Wasm compiler (nodejs/node#63421) ran out of memory on the tree-sitter grammars, so the AST track now keeps them on V8's baseline tier on Node 24 and later. That also cuts the TypeScript grammar's peak memory there from about 1.4 GB to about 0.1 GB, for a parse about 1.6x slower; Node 20 and 22 are unchanged (for [#860](https://github.com/Tencent/teamai-cli/issues/860)).
- `teamai remove mcp ambiguous` removes a server named `ambiguous` from `mcp/mcp.yaml`. The name matched the value `remove` used internally to mean "refused", so the command printed `Nothing was removed.`, exited 1, and gave no reason (for [#862](https://github.com/Tencent/teamai-cli/issues/862)).
Expand Down
2 changes: 2 additions & 0 deletions docs/designs/data-directory-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ directories do not authorize writes to rules excluded by the local configuration
For Copilot updates that only change `paths`, it compares the entire local file
with the rendered recorded versions before refreshing `applyTo`, preserving
locally edited headers rather than overwriting them on a body match alone.
Each copy it writes, in any format, is recorded in the checkout's `delivered`
(#822), so the next pull does not keep it as the member's edit.
A placed agent, which push does not
sync, is held when the team file has changed since any of those revisions, or
since it was added if one of them predates it (#823). That sync brings the
Expand Down
34 changes: 31 additions & 3 deletions docs/designs/multi-project-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@ 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
or edited stays, because push never counts such extras as changes and they may
never have been pushed, and one at a path another version has is named on each
pull.
pull. With a record of what teamai delivered (below), a file at such a path is
also removed when it is still what teamai wrote there, and one teamai has no
record of is the member's own and stays without a warning.

An item that cannot be used replaces nothing. A skill directory without
`SKILL.md` is not a skill: it is left out of the desired set, so the root skill
Expand Down Expand Up @@ -457,10 +459,36 @@ legacy mode each repeated name. `teamai env|mcp|hooks|models list`,
`teamai list <env|hooks|mcp> --source repo` and `teamai status` show where each
entry comes from.

### Local edits (#822)

Each checkout record (`lastPullByWorkspace[<checkout>]`, HOME's for the user
scope) carries `delivered`: the sha256 of the bytes teamai last wrote at each
skill, rule and agent file path. Pull and the pre-push sync update it when they
write, through the same state save. A copy is the member's edit only when it
has a record and no longer matches it. Pull keeps such a copy and names it: an
info line when the team version is unchanged, a warning when it has moved.
Push warns about such a copy as well (the SessionStart pull is silent), without
holding it, since the member may have merged the team change already. A
skill directory is one unit, and files only the member added are not recorded.
Tombstone cleanup keeps an edited copy the same way, and so does the rules
sweep of a rule no longer delivered (deleted from the team repo, or of a
namespace the member left). `--force` keeps edits;
deleting the copy and running `pull --force` takes the team version. A record
without `delivered` (the first pull on this version, a new worktree) protects
nothing, and a copy teamai never delivered to that path is overwritten as
before. A forced full sync elsewhere keeps each checkout's `delivered`.
`doctor` does not fail on a kept copy; next to another problem it lists one
as "changed by you (kept by pull)".

### Known gaps

- No pull protects a local edit from being overwritten, override transitions
included.
- `teamai remove`'s rules refresh and local-agent installs deliver rules and
skills as before: they overwrite a changed copy and record nothing. If the
team changes a copy they wrote before the next pull, that pull keeps it as an
edit.
- Step 3b and the inactive-namespace cleanup of skills and agents still compare
with the team source, not the record, so an untouched copy delivered at an
older revision stays there with a warning.

## Backward compatibility

Expand Down
6 changes: 4 additions & 2 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,8 @@ teamai pull --dry-run # Dry run, no actual changes

A manual `teamai pull` ends by running the `teamai doctor` checks and printing each one that failed, with its fix — including whether the skills it just reported syncing are readable on disk for every enabled tool. It prints nothing when they all pass, and the exit code is unchanged. The SessionStart hook path and `--dry-run` run no checks at all, so session startup stays as fast as before. Provider checks (`gh`/`gf` authentication) are left to `teamai doctor`: the pull just used the provider.

**Pull keeps a skill, rule or agent you changed.** For each checkout, pull records what it wrote at each skill, rule and agent path. On a full sync, a copy that no longer matches that record is kept, and pull names it, while the copies of other tools still update. A skill counts as one copy: a change to any of its team files keeps the whole skill, and files only you added do not count. If the team version has not changed, pull prints ``Kept <path>: you changed it since teamai delivered it. Share it with `teamai push`, or delete it and run `teamai pull --force` to get the team version back.`` If it has, pull warns and asks you to merge the team change into your copy before you push it, and `teamai push` warns about that copy too, since the SessionStart pull runs silently. `--force` keeps these copies too, and `--dry-run` prints `Would keep <path>` for each. When the team removes an item, a copy you changed stays, and pull names it. There is no record before your first full pull with this version, so that pull overwrites as earlier versions did, and your changes are protected from then on. The same goes for a new worktree's first pull, and for a copy teamai never delivered to that path. `teamai remove` and installs from the local agent still rewrite the team rules without this check. An older CLI that saves state drops the record.

> Project scope is isolated by default. When the current working directory contains a project-scope `.teamai/config.yaml`, `pull` processes that project and skips user scope unless the local config has `inheritUserScope: true`; in that case it first refreshes the safe user-resource channel. Without a project config in the current directory, `pull` processes user scope. User `env`, MCP definitions, sources, reporting, and writes remain isolated in project mode. Hooks are the one exception: a project scope's hooks are injected into your **HOME** tool settings (`~/.claude/settings.json`, …), not `<projectRoot>`, because the built-in hooks gate on the `cwd` handed to `hook-dispatch` and `~/.claude` always exists so the "installed tool" gate passes (see the Hooks section). In a directory with no teamai config (no project config and no user scope), the team hooks do nothing: no reminders, and no session or skill usage is recorded; only machine-level work runs (the CLI update check, the session-start pull, the local agent, and package hints a pull stashed). For the team hooks and skill usage, a project config that exists but cannot be read counts as none, never as the user scope or as a lower-priority project config (such as a legacy `.teamai/config.yaml`) behind it. `pull` follows the same rule: it syncs no scope there, prints ``Nothing was synced: <file>: <reason>. Fix the file, or move it aside and run `teamai init` to write a new one.`` and exits 1 (with `--silent`, it prints nothing and still exits 1); a session start there runs no pull, seeds no agent directory and stashes no package hint. A hook whose `cwd` was deleted (a session that outlives its worktree) keeps the scope its session last recorded, so the session's last events and skill uses stay with the project, and its share reminder follows the project's settings, instead of the user scope's. This needs the session's earlier events in the local event log, which compaction trims to active sessions, and does not cover Copilot, whose events record no directory. Self single-repo mode keeps its hooks in the business repo so they travel on clone.

With role-based skills enabled, `pull`'s skill sync source becomes the contents of `skills/<namespace>/`, expanded according to `primaryRole + additionalRoles` and flattened into each local AI tool's skills directory. `rules/<namespace>/` and `claudemd/<namespace>/` follow the `knowledge` namespaces, and a `docs/<namespace>/` follows the `docs` namespaces once one is declared (see [Docs](#docs)); `agents/<namespace>/` follows the role's `agents` namespaces (see [Agents Resource Type](#agents-resource-type)). `learnings/` at the root is shared with everyone, while `learnings/<project-id>/` subdirectories sync only for the directory's active projects (see [Multi-project](#multi-project-project-as-a-dimension-orthogonal-to-role)).
Expand Down Expand Up @@ -753,7 +755,7 @@ Exclusion rules take effect after role and tag filtering. When running `teamai p

### Push local resources

Before scanning, `push` refreshes unedited old rule copies from the team repo. For Copilot, it compares Markdown bodies independently of the generated `applyTo` header and renders updates in `.instructions.md` format. Local body edits are preserved. This applies to project rules and user rules under `COPILOT_HOME`.
Before scanning, `push` refreshes unedited old rule copies from the team repo. For Copilot, it compares Markdown bodies independently of the generated `applyTo` header and renders updates in `.instructions.md` format. Local body edits are preserved. This applies to project rules and user rules under `COPILOT_HOME`. Each copy it refreshes is recorded as delivered, so the next `teamai pull` still updates it instead of keeping it as your change.

When only the team's `paths` change, `push` also refreshes Copilot's `applyTo` if the local file still matches a recorded version's generated copy. A locally edited header is preserved in this case.

Expand Down Expand Up @@ -790,7 +792,7 @@ Choose namespace [1-3] (default: 1 = common):
- `--role`/`--project` places new resources only. An edit of a shared-root rule or agent stays at the shared root, and push says so
- A placed resource stays maintainable from the machine that published it. While its PR is open, the open-PR record routes a later edit of the author's own copy back to that PR; once the file is on the default branch, `state.json` records where push put it, so the edit goes back to the same file, and an agent published into a namespace this directory has not activated is still editable rather than skipped as having no active source
- `teamai remove rules <name>` accepts the bare name the author's copy carries as well as the published `<namespace>/<name>`; it reports which one it resolved to, and removes both the namespaced team file and the author's copy at the rules root. If the team repo cannot be refreshed first, or this machine's placement records cannot be updated and saved, `remove` stops with exit 1 and removes nothing, because either can resolve the name to the wrong files
- A local agent is an edit of the team agent it was delivered from: one in an active namespace first, then one this machine placed, then the shared-root agent either of them replaces. Only when none exists does `--role`/`--project` decide, and the agent is new in that namespace; if that namespace already holds an agent of that name, the agent is skipped rather than written over it, as a rule would be. Two active agents of one name stay ambiguous and are skipped, flag or not. The same agent name may exist in several namespaces, so a copy in an inactive one you did not name never blocks publishing yours. A placed agent that changed on the team since this checkout last synced it is held until you run `teamai pull`, because agents have no pre-push sync. In single-repo mode, a root copy under `.teamai/` that matches an older version of the file it was placed at is held too: nothing refreshes it, so it is an old copy rather than an edit
- A local agent is an edit of the team agent it was delivered from: one in an active namespace first, then one this machine placed, then the shared-root agent either of them replaces. Only when none exists does `--role`/`--project` decide, and the agent is new in that namespace; if that namespace already holds an agent of that name, the agent is skipped rather than written over it, as a rule would be. Two active agents of one name stay ambiguous and are skipped, flag or not. The same agent name may exist in several namespaces, so a copy in an inactive one you did not name never blocks publishing yours. A placed agent that changed on the team since this checkout last synced it is held, because agents have no pre-push sync. Pull keeps your changed copy, so save your edit, delete the copy, run `teamai pull --force`, reapply the edit and push again. In single-repo mode, a root copy under `.teamai/` that matches an older version of the file it was placed at is held too: nothing refreshes it, so it is an old copy rather than an edit
- A new resource is never placed on top of one that is already there. If the resolved namespace already holds that name, the push stops and names the file: pull and edit the existing copy, rename yours, or pick another namespace with `--role <ns>`
- An agent whose namespace is not active here stays editable through its placement record, and `pull` delivers it for the same reason, so your copy tracks the team file. It replaces a shared-root agent of the same name, as an active namespace's agent would. An active namespace holding that name wins: that agent is the one deployed here
- A resource awaiting review in an open PR keeps that PR's destination — unless this push names a namespace other than the one recorded (the shared root counts as one), in which case the flag decides, the open PR is left untouched, and the collision is reported
Expand Down
Loading
Loading