diff --git a/README.en.md b/README.en.md
deleted file mode 100644
index a71a02b..0000000
--- a/README.en.md
+++ /dev/null
@@ -1,323 +0,0 @@
-# TeamAI — The team harness for AI agents
-
-> [English](README.en.md) | [简体中文](README.md)
-
-[](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml)
-[](https://www.npmjs.com/package/teamai-cli)
-[](https://www.npmjs.com/package/teamai-cli)
-[](LICENSE)
-
-Make every AI coding agent work by the same harness. Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team.
-
-**Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync).
-
-> 📖 **Full usage guide:** [docs/usage-guide.md](docs/usage-guide.md) — covers everything from team creation to day-to-day use.
-> 📚 **Provider notes:** [docs/providers.md](docs/providers.md) — GitHub / TGit differences and auth setup.
-
-Questions or suggestions are welcome — please open a PR or an Issue and help build this project together.
-
-## Install
-
-```bash
-npm install -g teamai-cli
-```
-
-
-Tencent internal users: install @tencent/teamai-cli via tnpm
-
-```bash
-npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com
-```
-
-The two packages share identical source code; `@tencent/teamai-cli` is just the internal mirror of the public `teamai-cli`.
-
-
-## Quick Start
-
-### Team members
-
-```bash
-# User-scope init (default, resources installed under ~/)
-teamai init --repo yourteam/yourproject
-
-# Project-scope init (resources installed under the project directory)
-cd /path/to/my-project
-teamai init --repo yourteam/yourproject --scope project
-
-# Non-interactive mode (for CI/CD or AI-agent automation)
-teamai init --repo yourteam/yourproject --scope user --role hai_dev --force
-```
-
-### Admins
-
-First create the shared-experience repo on your git host (GitHub by default; TGit also supported) and grant write access to every team member.
-
-- **GitHub:** create with `gh repo create yourorg/yourproject --private` or via the UI. Then use Settings → Collaborators to add members, and set `master`/`main` as the default branch.
-- **TGit (Tencent Gongfeng):** create on [git.woa.com](https://git.woa.com/) and grant master permissions in bulk via user groups.
-
-The CLI picks a provider automatically from the repo URL:
-
-- `yourorg/yourrepo` or `https://github.com/yourorg/yourrepo` → GitHub
-- `https://git.woa.com/yourteam/yourrepo` → TGit
-
-## Commands
-
-| Command | Description |
-|---------|-------------|
-| `teamai init [--scope ] [--role ] [--force]` | Initialize (auto-installs gf CLI, OAuth login, links repo, registers member, configures reviewers, injects hooks) |
-| `teamai push [--all] [--role ]` | Push local new resources to a dedicated branch and open a Merge Request; new skills prompt interactively for a target namespace (override with `--role`) |
-| `teamai pull [--silent]` | Pull team resources and inject them into local AI tools (both scopes pulled sequentially) |
-| `teamai status` | Show the diff between local and the team repo |
-| `teamai list [type] [--source repo\|local\|all] [--agent ]` | List resources (skills\|rules\|docs\|env\|wiki). With `--source local` or `all`, scans skills directories of installed AI agents and tags each skill's origin (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) |
-| `teamai skill [list\|show ]` | List all skills by default; `show ` prints the skill's origin, contributors, installed-agent list, and description summary |
-| `teamai members` | List registered team members |
-| `teamai remove ` | Remove a resource from both the team repo and local, then open an MR (skills\|rules\|wiki) |
-| `teamai roles` | Manage team roles (`init`/`list`/`set`/`add`/`remove`/`update`) |
-| `teamai source` | Manage cross-team skill subscription sources (`add`/`remove`/`list`/`browse`) |
-| `teamai contribute --file [--scope ]` | Push an AI-generated experience document to the team repo |
-| `teamai recall ` | Search the team knowledge base, automatically merging user + project scope results |
-| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) |
-| `teamai hooks` | Manage AI-tool hooks (list / inject / remove) |
-| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` |
-| `teamai doctor` | Diagnose configuration problems |
-
-Global options:
-- `--dry-run` — preview mode, no real changes
-- `--verbose, -v` — verbose output
-
-## How It Works
-
-```
-Member A Member B
- create skill / write rules same
- │ │
- ▼ ▼
- teamai push teamai push
- │ │
- ▼ ▼
- create branch + MR create branch + MR
- │ │
- └──────► team git repo ◄─────────────┘
- │ ▲
- │ │ reviewer approves + merges MR
- ▼
- SessionStart hook → teamai pull
- auto-synced to every member's local
-```
-
-- `teamai push` creates a dedicated branch (`teamai/push//`), pushes it, then opens a Merge Request and assigns reviewers automatically.
-- `teamai init` lets you configure default reviewers (stored in the `reviewers` field of `teamai.yaml`).
-- `teamai init` injects hooks tailored to each tool's format (`SessionStart`, `Stop`, `PostToolUse`, `UserPromptSubmit`, etc.). During sessions the hooks run `teamai pull`, `teamai update`, tracking, dashboard updates, and so on (supports Claude Code, Codex, Claude Code Internal, Codex Internal, Cursor, CodeBuddy IDE, OpenClaw, WorkBuddy).
-- Skills sync to `~/.claude/skills/`, `~/.codex/skills/`, `~/.codex-internal/skills/`, `~/.claude-internal/skills/`, `~/.cursor/skills/`, `~/.codebuddy/skills/`.
-- Rules sync to each tool's rules directory and are merged into `CLAUDE.md` via marker comments (supported for claude, claude-internal, codebuddy).
-- Knowledge syncs to `~/.teamai/docs/`.
-- Learnings sync to `~/.teamai/learnings/` and back the recall index (shared team-wide, not partitioned by role).
-- Culture syncs the team culture file (`culture.md`): its frontmatter and body are compiled and injected into every AI tool's `CLAUDE.md`.
-
-## Role-scoped Skills
-
-When the team resource repo enables role-scoped directories, skills are organized under role namespaces. During `teamai init`, the CLI asks you to pick a `primaryRole` and optional `additionalRoles` and writes them to your local `config.yaml`.
-
-Remote repo layout convention:
-
-```text
-manifest/roles.yaml # role definitions
-skills/// # skills organized by namespace
-rules/ # global, not role-scoped
-```
-
-- `teamai pull` reads `manifest/roles.yaml` and only syncs skills under `primaryRole + additionalRoles` namespaces (unioned with tag-filter results).
-- Skills install flat from `skills///` into `/skills//` — the namespace layout is invisible to users.
-- If two activated namespaces contain a skill with the same name, `pull` fails outright to prevent silent overrides.
-- Skills outside both activated namespaces and tag-filter results are cleaned up automatically.
-- `rules/`, `docs/`, `learnings/` keep their original behavior and are not role-scoped (learnings are shared team-wide).
-
-Example config:
-
-```yaml
-primaryRole: hai
-additionalRoles:
- - pm
-resourceProfileVersion: 1
-```
-
-This syncs every skill from `skills/common/`, `skills/hai/`, and `skills/pm/`.
-
-## Role-scoped Pushing
-
-In a role-scoped repo, when you push a new skill the CLI auto-detects available namespaces and prompts:
-
-```bash
-# Interactive namespace selection (recommended)
-teamai push
-# Output:
-# Which namespace should new skills be pushed to?
-# 1. common
-# 2. hai
-# 3. pm
-# Choose namespace [1-3] (default: 1 = common):
-
-# Explicit target namespace
-teamai push --role pm
-```
-
-- With a `primaryRole`, the list expands from `manifest/roles.yaml`.
-- Without a `primaryRole`, namespaces are discovered by scanning the team repo's directory structure.
-- When only one namespace exists, it's selected automatically — no prompt.
-- `--role ` temporarily overrides the target namespace.
-- Modifying an existing skill keeps its original namespace — no reselection needed.
-
-On push, the CLI checks `SKILL.md`'s YAML frontmatter (`name`/`description`) and auto-fills anything missing, so you don't have to maintain it by hand.
-
-## Team Culture
-
-Create `culture.md` at the root of the team repo. Use YAML frontmatter for company/team info and the body for cultural guidelines:
-
-```markdown
----
-company:
- name: Acme Corp
- mission: Build great things
- values:
- - Innovation
- - Integrity
-team:
- name: Platform
- mission: Enable developers
- goals:
- - Ship v2.0
- - Improve test coverage
----
-
-## Coding Guidelines
-
-- Every PR needs at least one reviewer approval
-- Direct pushes to master are forbidden
-- Test coverage must stay above 80%
-```
-
-`teamai pull` compiles `culture.md` into structured content and injects it into every AI tool's `CLAUDE.md` (between `` and ``). AI coding assistants pick up the team culture on every session.
-
-## Cross-team Skill Subscription
-
-Use `teamai source` to subscribe to other teams' public skill repos. Their skills sync automatically on `pull`:
-
-```bash
-# Add a subscription source
-teamai source add https://github.com/other-team/teamai-public.git --name other-team
-
-# List subscribed sources
-teamai source list
-
-# Browse skills from a source
-teamai source browse other-team
-
-# Remove a subscription (and clean up its skills)
-teamai source remove other-team
-```
-
-Subscribed skills sync to your local machine on `teamai pull` and coexist with your own team's skills.
-
-## Scope
-
-TeamAI supports two scopes that can coexist:
-
-| Dimension | User Scope (default) | Project Scope |
-|-----------|---------------------|---------------|
-| **Install location** | under `~/` (e.g. `~/.claude/skills/`) | under the project (e.g. `/.claude/skills/`) |
-| **Config file** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` |
-| **Use case** | general team norms, cross-project skills | project-specific skills and rules |
-| **Init** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` |
-
-**Dual-scope cooperation:**
-- `teamai pull` pulls user and project scopes sequentially; they don't conflict.
-- `teamai contribute --scope user/project` lets you pick which repo to push to.
-- `teamai recall` merges knowledge bases from both scopes into a single ranking and tags each result with its origin `[user]` / `[project]`.
-- The `scope` field in the remote `teamai.yaml` locks the repo's type; member init must match.
-
-## Automatic Experience Sharing
-
-When an AI coding session ends, the Stop hook evaluates session value and prompts you to share:
-
-```
-AI coding session (ongoing...)
- │
- ▼ PostToolUse hook continuously tracks tool calls and skill usage
- │
- ▼ session ends (Stop hook fires)
- │
- ├─ Smart scoring: tool-call count + tool diversity + skill usage + error retries + session duration
- │ (extracted from dashboard events.jsonl, one-shot, out of 100)
- │
- ├─ Score < 35 → stay silent (too few or too uniform calls, not worth summarizing)
- │
- ▼ Score ≥ 35
- │
- AI: "This session was productive — consider running /teamai-share-learnings to share."
- │
- ▼ user accepts
- │
- /teamai-share-learnings (AI sub-agent)
- ├─ AI summarizes the session's lessons
- ├─ Generates a Markdown document
- └─ teamai contribute --file → pushes directly to the team repo's learnings/
-```
-
-- `/teamai-share-learnings` is a built-in CLI skill, deployed locally by `teamai pull/init`.
-- Each session is prompted at most once (de-duplicated); you can always ignore it.
-- The document lands directly in `learnings/` and is visible to teammates on their next `pull`.
-
-## Team Knowledge Recall
-
-`teamai recall` implements the "read" side of the knowledge flywheel — the AI can search across accumulated team experience docs:
-
-```
-contribute (write) → pull (sync + index) → recall (search) → upvote (vote) → better ranking
-```
-
-```bash
-$ teamai recall "fuse port"
-[1/2] MR review caught a FUSE port-conflict bug ★1 [user]
-Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s
-
-[2/2] FUSE deployment configuration best practices [project]
-Author: alice | Score: 12.0 | Tags: fuse, deploy
-```
-
-- **Dual-scope merged search:** automatically merges user and project scope knowledge bases, each result tagged with its origin.
-- Hybrid CJK + English search (Intl.Segmenter + CJK bigrams).
-- Searches implicitly upvote matched docs; good docs naturally float up over time.
-- Votes are written to each scope's own repo, so attribution stays correct.
-
-## Update
-
-```bash
-teamai update # auto-detect and upgrade to latest
-npm update -g teamai-cli # or trigger an npm upgrade manually
-```
-
-`teamai update` picks the registry based on the installed package name:
-
-- `teamai-cli` → public npm (`https://registry.npmjs.org`)
-- `@tencent/teamai-cli` → internal tnpm (`http://r.tnpm.oa.com`)
-
-To override the registry manually, set `TEAMAI_NPM_REGISTRY=`.
-
-### Auto-update Control
-
-Auto-update runs on the Stop hook at the end of a session. It can be controlled at two layers:
-
-| Layer | File | Field | Allowed values |
-|-------|------|-------|----------------|
-| Team default | `teamai.yaml` | `autoUpdate` | `true` (default) / `false` |
-| User override | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
-
-The user-level `updatePolicy` always wins over the team-level `autoUpdate`.
-
-## License
-
-[MIT](LICENSE)
-
-## Contributing
-
-PRs are welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) first.
diff --git a/README.md b/README.md
index cec6456..2c29215 100644
--- a/README.md
+++ b/README.md
@@ -1,138 +1,141 @@
# TeamAI — The team harness for AI agents
-> [English](README.en.md) | [简体中文](README.md)
+> [English](README.md) | [简体中文](README.zh-CN.md)
[](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/teamai-cli)
[](https://www.npmjs.com/package/teamai-cli)
[](LICENSE)
-让每个 AI 编程助手都按同一套标准工作。通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。
+Make every AI coding agent work by the same harness.
-**支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。
+Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team.
-> 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。
-> 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。
+**Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync).
-如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。
+> 📖 **Full usage guide:** [docs/usage-guide.md](docs/usage-guide.md) — covers everything from team creation to day-to-day use.
-## 安装
+> 📚 **Provider notes:** [docs/providers.md](docs/providers.md) — GitHub / TGit differences and auth setup.
+
+Questions or suggestions are welcome — please open a PR or an Issue and help build this project together.
+
+## Install
```bash
npm install -g teamai-cli
```
-腾讯内部用户:通过 tnpm 安装 @tencent/teamai-cli
+Tencent internal users: install @tencent/teamai-cli via tnpm
```bash
npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com
```
-两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。
+The two packages share identical source code; `@tencent/teamai-cli` is just the internal mirror of the public `teamai-cli`.
-## 快速开始
+## Quick Start
-### 团队成员
+### Team members
```bash
-# 用户级初始化(默认,资源安装到 ~/)
+# User-scope init (default, resources installed under ~/)
teamai init --repo yourteam/yourproject
-# 项目级初始化(资源安装到项目目录下)
+# Project-scope init (resources installed under the project directory)
cd /path/to/my-project
teamai init --repo yourteam/yourproject --scope project
-# 非交互模式(适合 CI/CD 或 AI agent 自动化)
+# Non-interactive mode (for CI/CD or AI-agent automation)
teamai init --repo yourteam/yourproject --scope user --role hai_dev --force
```
-### 管理员
+### Admins
-先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。
+First create the shared-experience repo on your git host (GitHub by default; TGit also supported) and grant write access to every team member.
-- **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。
-- **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。
+- **GitHub:** create with `gh repo create yourorg/yourproject --private` or via the UI. Then use Settings → Collaborators to add members, and set `master`/`main` as the default branch.
+- **TGit (Tencent Gongfeng):** create on [git.woa.com](https://git.woa.com/) and grant master permissions in bulk via user groups.
-CLI 会根据用户传入的 repo URL 自动选择 provider:
+The CLI picks a provider automatically from the repo URL:
-- `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub
+- `yourorg/yourrepo` or `https://github.com/yourorg/yourrepo` → GitHub
- `https://git.woa.com/yourteam/yourrepo` → TGit
-## 命令
-
-| 命令 | 说明 |
-|------|------|
-| `teamai init [--scope ] [--role ] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) |
-| `teamai push [--all] [--role ]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 |
-| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) |
-| `teamai status` | 查看本地 vs 团队仓库差异 |
-| `teamai list [type] [--source repo\|local\|all] [--agent ]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) |
-| `teamai skill [list\|show ]` | 默认列出全部 skill;`show ` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 |
-| `teamai members` | 列出已注册的团队成员 |
-| `teamai remove ` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) |
-| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) |
-| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) |
-| `teamai contribute --file [--scope ]` | 将 AI 生成的经验文档推送到团队仓库 |
-| `teamai recall ` | 搜索团队知识库,自动合并 user + project 双 scope 结果 |
-| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) |
-| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) |
-| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ |
-| `teamai doctor` | 诊断配置问题 |
-
-全局选项:
-- `--dry-run` — 预览模式,不做实际变更
-- `--verbose, -v` — 详细输出
-
-## 工作原理
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| `teamai init [--scope ] [--role ] [--force]` | Initialize (auto-installs gf CLI, OAuth login, links repo, registers member, configures reviewers, injects hooks) |
+| `teamai push [--all] [--role ]` | Push local new resources to a dedicated branch and open a Merge Request; new skills prompt interactively for a target namespace (override with `--role`) |
+| `teamai pull [--silent]` | Pull team resources and inject them into local AI tools (both scopes pulled sequentially) |
+| `teamai status` | Show the diff between local and the team repo |
+| `teamai list [type] [--source repo\|local\|all] [--agent ]` | List resources (skills\|rules\|docs\|env\|wiki). With `--source local` or `all`, scans skills directories of installed AI agents and tags each skill's origin (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) |
+| `teamai skill [list\|show ]` | List all skills by default; `show ` prints the skill's origin, contributors, installed-agent list, and description summary |
+| `teamai members` | List registered team members |
+| `teamai remove ` | Remove a resource from both the team repo and local, then open an MR (skills\|rules\|wiki) |
+| `teamai roles` | Manage team roles (`init`/`list`/`set`/`add`/`remove`/`update`) |
+| `teamai source` | Manage cross-team skill subscription sources (`add`/`remove`/`list`/`browse`) |
+| `teamai contribute --file [--scope ]` | Push an AI-generated experience document to the team repo |
+| `teamai recall ` | Search the team knowledge base, automatically merging user + project scope results |
+| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) |
+| `teamai hooks` | Manage AI-tool hooks (list / inject / remove) |
+| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` |
+| `teamai doctor` | Diagnose configuration problems |
+
+Global options:
+- `--dry-run` — preview mode, no real changes
+- `--verbose, -v` — verbose output
+
+## How It Works
```
-成员 A 成员 B
- 创建 skill / 写规则 同上
+Member A Member B
+ create skill / write rules same
│ │
▼ ▼
teamai push teamai push
│ │
▼ ▼
- 创建分支 + MR 创建分支 + MR
+ create branch + MR create branch + MR
│ │
- └──────► 团队git仓库 ◄──────────────┘
+ └──────► team git repo ◄─────────────┘
│ ▲
- │ │ reviewer 审批合并 MR
+ │ │ reviewer approves + merges MR
▼
SessionStart hook → teamai pull
- 自动拉取到所有成员本地
+ auto-synced to every member's local
```
-- `teamai push` 会创建独立分支(`teamai/push//`),推送后自动创建 Merge Request 并指派 reviewers
-- `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段)
-- `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy)
-- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/`
-- Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy)
-- Knowledge 同步到 `~/.teamai/docs/`
-- Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分)
-- Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md`
+- `teamai push` creates a dedicated branch (`teamai/push//`), pushes it, then opens a Merge Request and assigns reviewers automatically.
+- `teamai init` lets you configure default reviewers (stored in the `reviewers` field of `teamai.yaml`).
+- `teamai init` injects hooks tailored to each tool's format (`SessionStart`, `Stop`, `PostToolUse`, `UserPromptSubmit`, etc.). During sessions the hooks run `teamai pull`, `teamai update`, tracking, dashboard updates, and so on (supports Claude Code, Codex, Claude Code Internal, Codex Internal, Cursor, CodeBuddy IDE, OpenClaw, WorkBuddy).
+- Skills sync to `~/.claude/skills/`, `~/.codex/skills/`, `~/.codex-internal/skills/`, `~/.claude-internal/skills/`, `~/.cursor/skills/`, `~/.codebuddy/skills/`.
+- Rules sync to each tool's rules directory and are merged into `CLAUDE.md` via marker comments (supported for claude, claude-internal, codebuddy).
+- Knowledge syncs to `~/.teamai/docs/`.
+- Learnings sync to `~/.teamai/learnings/` and back the recall index (shared team-wide, not partitioned by role).
+- Culture syncs the team culture file (`culture.md`): its frontmatter and body are compiled and injected into every AI tool's `CLAUDE.md`.
-## 角色化 Skills
+## Role-scoped Skills
-当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。
+When the team resource repo enables role-scoped directories, skills are organized under role namespaces. During `teamai init`, the CLI asks you to pick a `primaryRole` and optional `additionalRoles` and writes them to your local `config.yaml`.
-远端仓库目录约定:
+Remote repo layout convention:
```text
-manifest/roles.yaml # 角色定义
-skills/// # 按 namespace 组织的 skills
-rules/ # 全局,不做角色拆分
+manifest/roles.yaml # role definitions
+skills/// # skills organized by namespace
+rules/ # global, not role-scoped
```
-- `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。
-- Skills 从 `skills///` 拍平安装到本地 `/skills//`,用户无感知 namespace 结构。
-- 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。
-- 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。
-- `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。
+- `teamai pull` reads `manifest/roles.yaml` and only syncs skills under `primaryRole + additionalRoles` namespaces (unioned with tag-filter results).
+- Skills install flat from `skills///` into `/skills//` — the namespace layout is invisible to users.
+- If two activated namespaces contain a skill with the same name, `pull` fails outright to prevent silent overrides.
+- Skills outside both activated namespaces and tag-filter results are cleaned up automatically.
+- `rules/`, `docs/`, `learnings/` keep their original behavior and are not role-scoped (learnings are shared team-wide).
-配置示例:
+Example config:
```yaml
primaryRole: hai
@@ -141,37 +144,37 @@ additionalRoles:
resourceProfileVersion: 1
```
-这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。
+This syncs every skill from `skills/common/`, `skills/hai/`, and `skills/pm/`.
-## 角色化推送
+## Role-scoped Pushing
-角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择:
+In a role-scoped repo, when you push a new skill the CLI auto-detects available namespaces and prompts:
```bash
-# 交互式选择命名空间(推荐)
+# Interactive namespace selection (recommended)
teamai push
-# 输出:
+# Output:
# Which namespace should new skills be pushed to?
# 1. common
# 2. hai
# 3. pm
# Choose namespace [1-3] (default: 1 = common):
-# 显式指定目标 namespace
+# Explicit target namespace
teamai push --role pm
```
-- 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表
-- 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace
-- 单一命名空间时自动选中,无需交互
-- `--role ` 可临时覆盖目标 namespace
-- 修改已有 skill 时自动保持原 namespace,无需重新选择
+- With a `primaryRole`, the list expands from `manifest/roles.yaml`.
+- Without a `primaryRole`, namespaces are discovered by scanning the team repo's directory structure.
+- When only one namespace exists, it's selected automatically — no prompt.
+- `--role ` temporarily overrides the target namespace.
+- Modifying an existing skill keeps its original namespace — no reselection needed.
-推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。
+On push, the CLI checks `SKILL.md`'s YAML frontmatter (`name`/`description`) and auto-fills anything missing, so you don't have to maintain it by hand.
-## 团队文化(Culture)
+## Team Culture
-在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引:
+Create `culture.md` at the root of the team repo. Use YAML frontmatter for company/team info and the body for cultural guidelines:
```markdown
---
@@ -189,135 +192,135 @@ team:
- Improve test coverage
---
-## 编码准则
+## Coding Guidelines
-- 所有 PR 必须有至少一个 reviewer 审批
-- 禁止直接 push master
-- 测试覆盖率不低于 80%
+- Every PR needs at least one reviewer approval
+- Direct pushes to master are forbidden
+- Test coverage must stay above 80%
```
-`teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`` / `` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。
+`teamai pull` compiles `culture.md` into structured content and injects it into every AI tool's `CLAUDE.md` (between `` and ``). AI coding assistants pick up the team culture on every session.
-## 跨团队 Skill 订阅
+## Cross-team Skill Subscription
-通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills:
+Use `teamai source` to subscribe to other teams' public skill repos. Their skills sync automatically on `pull`:
```bash
-# 添加订阅源
-teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team
+# Add a subscription source
+teamai source add https://github.com/other-team/teamai-public.git --name other-team
-# 查看已订阅的源
+# List subscribed sources
teamai source list
-# 浏览订阅源的 skills
+# Browse skills from a source
teamai source browse other-team
-# 移除订阅(同时清理其 skills)
+# Remove a subscription (and clean up its skills)
teamai source remove other-team
```
-订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。
+Subscribed skills sync to your local machine on `teamai pull` and coexist with your own team's skills.
-## Scope(作用域)
+## Scope
-TeamAI 支持两种 scope,可以共存:
+TeamAI supports two scopes that can coexist:
-| 维度 | User Scope(默认) | Project Scope |
-|------|-------------------|---------------|
-| **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `/.claude/skills/`) |
-| **配置文件** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` |
-| **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 |
-| **初始化** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` |
+| Dimension | User Scope (default) | Project Scope |
+|-----------|---------------------|---------------|
+| **Install location** | under `~/` (e.g. `~/.claude/skills/`) | under the project (e.g. `/.claude/skills/`) |
+| **Config file** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` |
+| **Use case** | general team norms, cross-project skills | project-specific skills and rules |
+| **Init** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` |
-**双 scope 协同:**
-- `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突
-- `teamai contribute --scope user/project` 可显式选择推送到哪个仓库
-- `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]`
-- 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配
+**Dual-scope cooperation:**
+- `teamai pull` pulls user and project scopes sequentially; they don't conflict.
+- `teamai contribute --scope user/project` lets you pick which repo to push to.
+- `teamai recall` merges knowledge bases from both scopes into a single ranking and tags each result with its origin `[user]` / `[project]`.
+- The `scope` field in the remote `teamai.yaml` locks the repo's type; member init must match.
-## 经验自动分享
+## Automatic Experience Sharing
-当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享:
+When an AI coding session ends, the Stop hook evaluates session value and prompts you to share:
```
-AI coding session (持续工作中...)
+AI coding session (ongoing...)
│
- ▼ PostToolUse hook 持续追踪工具调用和 skill 使用
+ ▼ PostToolUse hook continuously tracks tool calls and skill usage
│
- ▼ 会话结束(Stop hook 触发)
+ ▼ session ends (Stop hook fires)
│
- ├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长
- │ (从 dashboard events.jsonl 提取,一次性评估,满分 100)
+ ├─ Smart scoring: tool-call count + tool diversity + skill usage + error retries + session duration
+ │ (extracted from dashboard events.jsonl, one-shot, out of 100)
│
- ├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值)
+ ├─ Score < 35 → stay silent (too few or too uniform calls, not worth summarizing)
│
- ▼ 分数 ≥ 35
+ ▼ Score ≥ 35
│
- AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验"
+ AI: "This session was productive — consider running /teamai-share-learnings to share."
│
- ▼ 用户同意
+ ▼ user accepts
│
/teamai-share-learnings (AI sub-agent)
- ├─ AI 总结本次 session 的经验
- ├─ 生成 Markdown 文档
- └─ teamai contribute --file → 直接 push 到团队仓库 learnings/
+ ├─ AI summarizes the session's lessons
+ ├─ Generates a Markdown document
+ └─ teamai contribute --file → pushes directly to the team repo's learnings/
```
-- `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地
-- 每个 session 最多提示一次(去重),用户可以忽略
-- 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见
+- `/teamai-share-learnings` is a built-in CLI skill, deployed locally by `teamai pull/init`.
+- Each session is prompted at most once (de-duplicated); you can always ignore it.
+- The document lands directly in `learnings/` and is visible to teammates on their next `pull`.
-## 团队知识回忆
+## Team Knowledge Recall
-`teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档:
+`teamai recall` implements the "read" side of the knowledge flywheel — the AI can search across accumulated team experience docs:
```
-contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化
+contribute (write) → pull (sync + index) → recall (search) → upvote (vote) → better ranking
```
```bash
-$ teamai recall "fuse 端口"
-[1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user]
+$ teamai recall "fuse port"
+[1/2] MR review caught a FUSE port-conflict bug ★1 [user]
Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s
-[2/2] FUSE 部署配置最佳实践 [project]
+[2/2] FUSE deployment configuration best practices [project]
Author: alice | Score: 12.0 | Tags: fuse, deploy
```
-- **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源
-- Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams)
-- 搜索自动投票,好文档自然浮到顶部
-- 投票按 scope 分别写入各自的 repo,归属正确
+- **Dual-scope merged search:** automatically merges user and project scope knowledge bases, each result tagged with its origin.
+- Hybrid CJK + English search (Intl.Segmenter + CJK bigrams).
+- Searches implicitly upvote matched docs; good docs naturally float up over time.
+- Votes are written to each scope's own repo, so attribution stays correct.
-## 更新
+## Update
```bash
-teamai update # 自动检测并升级到最新版
-npm update -g teamai-cli # 或手动触发 npm 升级
+teamai update # auto-detect and upgrade to latest
+npm update -g teamai-cli # or trigger an npm upgrade manually
```
-`teamai update` 会根据当前安装的包名自动选择 registry:
+`teamai update` picks the registry based on the installed package name:
-- `teamai-cli` → 公网 npm (`https://registry.npmjs.org`)
-- `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`)
+- `teamai-cli` → public npm (`https://registry.npmjs.org`)
+- `@tencent/teamai-cli` → internal tnpm (`http://r.tnpm.oa.com`)
-如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=`。
+To override the registry manually, set `TEAMAI_NPM_REGISTRY=`.
-### 自动更新控制
+### Auto-update Control
-自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制:
+Auto-update runs on the Stop hook at the end of a session. It can be controlled at two layers:
-| 配置层级 | 文件 | 字段 | 可选值 |
-|---------|------|------|-------|
-| 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` |
-| 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
+| Layer | File | Field | Allowed values |
+|-------|------|-------|----------------|
+| Team default | `teamai.yaml` | `autoUpdate` | `true` (default) / `false` |
+| User override | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
-用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。
+The user-level `updatePolicy` always wins over the team-level `autoUpdate`.
-## 许可证
+## License
[MIT](LICENSE)
-## 贡献
+## Contributing
-欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。
+PRs are welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) first.
diff --git a/README.zh-CN.md b/README.zh-CN.md
new file mode 100644
index 0000000..536f933
--- /dev/null
+++ b/README.zh-CN.md
@@ -0,0 +1,326 @@
+# TeamAI — The team harness for AI agents
+
+> [English](README.md) | [简体中文](README.zh-CN.md)
+
+[](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml)
+[](https://www.npmjs.com/package/teamai-cli)
+[](https://www.npmjs.com/package/teamai-cli)
+[](LICENSE)
+
+让每个 AI 编程助手都按同一套标准工作。
+
+通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。
+
+**支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。
+
+> 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。
+
+> 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。
+
+如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。
+
+## 安装
+
+```bash
+npm install -g teamai-cli
+```
+
+
+腾讯内部用户:通过 tnpm 安装 @tencent/teamai-cli
+
+```bash
+npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com
+```
+
+两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。
+
+
+## 快速开始
+
+### 团队成员
+
+```bash
+# 用户级初始化(默认,资源安装到 ~/)
+teamai init --repo yourteam/yourproject
+
+# 项目级初始化(资源安装到项目目录下)
+cd /path/to/my-project
+teamai init --repo yourteam/yourproject --scope project
+
+# 非交互模式(适合 CI/CD 或 AI agent 自动化)
+teamai init --repo yourteam/yourproject --scope user --role hai_dev --force
+```
+
+### 管理员
+
+先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。
+
+- **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。
+- **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。
+
+CLI 会根据用户传入的 repo URL 自动选择 provider:
+
+- `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub
+- `https://git.woa.com/yourteam/yourrepo` → TGit
+
+## 命令
+
+| 命令 | 说明 |
+|------|------|
+| `teamai init [--scope ] [--role ] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) |
+| `teamai push [--all] [--role ]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 |
+| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) |
+| `teamai status` | 查看本地 vs 团队仓库差异 |
+| `teamai list [type] [--source repo\|local\|all] [--agent ]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) |
+| `teamai skill [list\|show ]` | 默认列出全部 skill;`show ` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 |
+| `teamai members` | 列出已注册的团队成员 |
+| `teamai remove ` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) |
+| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) |
+| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) |
+| `teamai contribute --file [--scope ]` | 将 AI 生成的经验文档推送到团队仓库 |
+| `teamai recall ` | 搜索团队知识库,自动合并 user + project 双 scope 结果 |
+| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) |
+| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) |
+| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ |
+| `teamai doctor` | 诊断配置问题 |
+
+全局选项:
+- `--dry-run` — 预览模式,不做实际变更
+- `--verbose, -v` — 详细输出
+
+## 工作原理
+
+```
+成员 A 成员 B
+ 创建 skill / 写规则 同上
+ │ │
+ ▼ ▼
+ teamai push teamai push
+ │ │
+ ▼ ▼
+ 创建分支 + MR 创建分支 + MR
+ │ │
+ └──────► 团队git仓库 ◄──────────────┘
+ │ ▲
+ │ │ reviewer 审批合并 MR
+ ▼
+ SessionStart hook → teamai pull
+ 自动拉取到所有成员本地
+```
+
+- `teamai push` 会创建独立分支(`teamai/push//`),推送后自动创建 Merge Request 并指派 reviewers
+- `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段)
+- `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy)
+- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/`
+- Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy)
+- Knowledge 同步到 `~/.teamai/docs/`
+- Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分)
+- Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md`
+
+## 角色化 Skills
+
+当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。
+
+远端仓库目录约定:
+
+```text
+manifest/roles.yaml # 角色定义
+skills/// # 按 namespace 组织的 skills
+rules/ # 全局,不做角色拆分
+```
+
+- `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。
+- Skills 从 `skills///` 拍平安装到本地 `/skills//`,用户无感知 namespace 结构。
+- 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。
+- 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。
+- `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。
+
+配置示例:
+
+```yaml
+primaryRole: hai
+additionalRoles:
+ - pm
+resourceProfileVersion: 1
+```
+
+这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。
+
+## 角色化推送
+
+角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择:
+
+```bash
+# 交互式选择命名空间(推荐)
+teamai push
+# 输出:
+# Which namespace should new skills be pushed to?
+# 1. common
+# 2. hai
+# 3. pm
+# Choose namespace [1-3] (default: 1 = common):
+
+# 显式指定目标 namespace
+teamai push --role pm
+```
+
+- 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表
+- 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace
+- 单一命名空间时自动选中,无需交互
+- `--role ` 可临时覆盖目标 namespace
+- 修改已有 skill 时自动保持原 namespace,无需重新选择
+
+推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。
+
+## 团队文化(Culture)
+
+在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引:
+
+```markdown
+---
+company:
+ name: Acme Corp
+ mission: Build great things
+ values:
+ - Innovation
+ - Integrity
+team:
+ name: Platform
+ mission: Enable developers
+ goals:
+ - Ship v2.0
+ - Improve test coverage
+---
+
+## 编码准则
+
+- 所有 PR 必须有至少一个 reviewer 审批
+- 禁止直接 push master
+- 测试覆盖率不低于 80%
+```
+
+`teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`` / `` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。
+
+## 跨团队 Skill 订阅
+
+通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills:
+
+```bash
+# 添加订阅源
+teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team
+
+# 查看已订阅的源
+teamai source list
+
+# 浏览订阅源的 skills
+teamai source browse other-team
+
+# 移除订阅(同时清理其 skills)
+teamai source remove other-team
+```
+
+订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。
+
+## Scope(作用域)
+
+TeamAI 支持两种 scope,可以共存:
+
+| 维度 | User Scope(默认) | Project Scope |
+|------|-------------------|---------------|
+| **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `/.claude/skills/`) |
+| **配置文件** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` |
+| **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 |
+| **初始化** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` |
+
+**双 scope 协同:**
+- `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突
+- `teamai contribute --scope user/project` 可显式选择推送到哪个仓库
+- `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]`
+- 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配
+
+## 经验自动分享
+
+当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享:
+
+```
+AI coding session (持续工作中...)
+ │
+ ▼ PostToolUse hook 持续追踪工具调用和 skill 使用
+ │
+ ▼ 会话结束(Stop hook 触发)
+ │
+ ├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长
+ │ (从 dashboard events.jsonl 提取,一次性评估,满分 100)
+ │
+ ├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值)
+ │
+ ▼ 分数 ≥ 35
+ │
+ AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验"
+ │
+ ▼ 用户同意
+ │
+ /teamai-share-learnings (AI sub-agent)
+ ├─ AI 总结本次 session 的经验
+ ├─ 生成 Markdown 文档
+ └─ teamai contribute --file → 直接 push 到团队仓库 learnings/
+```
+
+- `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地
+- 每个 session 最多提示一次(去重),用户可以忽略
+- 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见
+
+## 团队知识回忆
+
+`teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档:
+
+```
+contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化
+```
+
+```bash
+$ teamai recall "fuse 端口"
+[1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user]
+Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s
+
+[2/2] FUSE 部署配置最佳实践 [project]
+Author: alice | Score: 12.0 | Tags: fuse, deploy
+```
+
+- **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源
+- Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams)
+- 搜索自动投票,好文档自然浮到顶部
+- 投票按 scope 分别写入各自的 repo,归属正确
+
+## 更新
+
+```bash
+teamai update # 自动检测并升级到最新版
+npm update -g teamai-cli # 或手动触发 npm 升级
+```
+
+`teamai update` 会根据当前安装的包名自动选择 registry:
+
+- `teamai-cli` → 公网 npm (`https://registry.npmjs.org`)
+- `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`)
+
+如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=`。
+
+### 自动更新控制
+
+自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制:
+
+| 配置层级 | 文件 | 字段 | 可选值 |
+|---------|------|------|-------|
+| 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` |
+| 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
+
+用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。
+
+## 许可证
+
+[MIT](LICENSE)
+
+## 贡献
+
+欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。
diff --git a/package-lock.json b/package-lock.json
index d2e577f..3749bf4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "teamai-cli",
- "version": "0.16.3",
+ "version": "0.16.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 82bb01f..81021f2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "teamai-cli",
- "version": "0.16.3",
+ "version": "0.16.4",
"description": "TeamAI — the team harness for AI agents (skill sync + shared knowledge base, powered by Git)",
"type": "module",
"bin": {
diff --git a/skills/teamai-wiki/SKILL.md b/skills/teamai-wiki/SKILL.md
index a78e3fe..3885b97 100644
--- a/skills/teamai-wiki/SKILL.md
+++ b/skills/teamai-wiki/SKILL.md
@@ -243,10 +243,7 @@ When invoked, first determine the subcommand (init/ingest/query/lint/status/expo
#### Step 1 — Parse arguments
-- `WIKI_DIR`: wiki 目录路径。按以下顺序检测:
- 1. team repo 中的 `wiki/` 目录(如果当前项目已通过 `teamai init` 配置)→ 首选
- 2. `~/.claude-internal/wiki/` 或 `~/.claude/wiki/`(本地 AI 工具 wiki 目录)
- 3. 当前目录的 `./wiki/`(fallback)
+- `WIKI_DIR`: 固定为 `~/.teamai/wiki/`(teamai push/pull 同步的标准路径)
- `SOURCE_DIR`: 可选的 `dir` 参数
如果 `WIKI_DIR` 已经存在且包含 `_metadata.json`,提示用户已经初始化过,询问是否要重新初始化。
@@ -1009,7 +1006,7 @@ Wiki exported to:
8. **_metadata.json 是真相来源** — 页面列表、文件哈希、链接图都以此为准。
9. **命名一致性** — 文件名 kebab-case,标题 Title Case 或人类可读中文。
10. **幂等性** — 重复 ingest 同一源目录应产生相同结果(不会重复创建页面)。
-11. **wiki 路径推断** — 优先使用 team repo 的 wiki/ 目录(通过 `teamai pull` 同步到本地);其次检查 `~/.claude-internal/wiki/` 或 `~/.claude/wiki/`;最后 fallback 到 `./wiki/`。
+11. **wiki 路径** — 固定使用 `~/.teamai/wiki/`,与 teamai push/pull 对齐。
12. **Obsidian 兼容** — 所有 `[[links]]` 使用 Obsidian 格式,方便用户在 Obsidian 中直接浏览。
13. **语言** — Wiki 页面内容默认使用中文撰写,技术术语保持英文原文。
14. **智能分类** — LLM 根据内容自动判断页面归属哪个分类目录,无需用户指定。
diff --git a/src/__tests__/e2e/e2e.test.ts b/src/__tests__/e2e/e2e.test.ts
index cb2160b..77fc55f 100644
--- a/src/__tests__/e2e/e2e.test.ts
+++ b/src/__tests__/e2e/e2e.test.ts
@@ -668,19 +668,25 @@ describe('init project scope (sandboxed)', () => {
const stdinAllBlanks = '\n\n\n\n\n';
const result = await runCLIWithEnv(
- ['init', '--scope', 'project', '--repo', fullUrl, '--force'],
+ ['init', '--scope', 'project', '--repo', fullUrl, '--role', 'common', '--force'],
{
// GitHub: gh CLI / REST honors GITHUB_TOKEN
// TGit: gf CLI honors TGIT_TOKEN
GITHUB_TOKEN: process.env.TEAMAI_TEST_TOKEN ?? '',
TGIT_TOKEN: process.env.TEAMAI_TEST_TOKEN ?? '',
HOME: sandbox, // isolate from user-scope ~/.teamai
+ // Git identity must be set explicitly because HOME override
+ // hides the global .gitconfig written by CI setup steps.
+ GIT_AUTHOR_NAME: 'TeamAI CI',
+ GIT_AUTHOR_EMAIL: 'ci@teamai.test',
+ GIT_COMMITTER_NAME: 'TeamAI CI',
+ GIT_COMMITTER_EMAIL: 'ci@teamai.test',
},
stdinAllBlanks,
sandbox, // cwd = sandbox, so .teamai/ lands here
);
- expect(result.code).toBe(0);
+ expect(result.code, `init failed with output:\n${result.output}`).toBe(0);
expect(fs.existsSync(path.join(sandbox, '.teamai', 'config.yaml'))).toBe(true);
// Verify the project-scope config has the expected shape
diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts
index dceb482..4c21df1 100644
--- a/src/__tests__/skills.test.ts
+++ b/src/__tests__/skills.test.ts
@@ -654,6 +654,30 @@ scope: 'user',
const content = await fse.readFile(contribPath, 'utf-8');
expect(content).toBe('testuser\n');
});
+
+ it('should NOT copy .git directory when skill source is a git repo', async () => {
+ const localSkillDir = path.join(homeDir, '.claude/skills', 'git-skill');
+ await fse.ensureDir(localSkillDir);
+ await fse.writeFile(path.join(localSkillDir, 'SKILL.md'), '# Git Skill\nContent here');
+ await fse.writeFile(path.join(localSkillDir, 'helper.py'), 'print("hello")');
+ // Simulate a .git directory (as if skill was cloned from a git repo)
+ await fse.ensureDir(path.join(localSkillDir, '.git', 'objects'));
+ await fse.writeFile(path.join(localSkillDir, '.git', 'HEAD'), 'ref: refs/heads/main');
+
+ const item = {
+ name: 'git-skill',
+ type: 'skills' as const,
+ sourcePath: localSkillDir,
+ relativePath: 'skills/git-skill',
+ };
+
+ await handler.pushItem(item, teamConfig, localConfig);
+
+ const destDir = path.join(localConfig.repo.localPath, 'skills', 'git-skill');
+ expect(await fse.pathExists(path.join(destDir, 'SKILL.md'))).toBe(true);
+ expect(await fse.pathExists(path.join(destDir, 'helper.py'))).toBe(true);
+ expect(await fse.pathExists(path.join(destDir, '.git'))).toBe(false);
+ });
});
describe('SkillsHandler.readContributors', () => {
diff --git a/src/__tests__/wiki-bugfix.test.ts b/src/__tests__/wiki-bugfix.test.ts
index 71998a4..39e5fff 100644
--- a/src/__tests__/wiki-bugfix.test.ts
+++ b/src/__tests__/wiki-bugfix.test.ts
@@ -12,6 +12,11 @@
* `REMOVABLE_TYPES` didn't include 'wiki'.
* BUG #4 — `teamai pull` didn't honor wiki tombstones: a `wiki/.removed`
* entry in the team repo never deleted the local copy.
+ *
+ * NOTE (refactored for shared wiki location):
+ * With the wiki refactoring, BUG #4 no longer iterates through per-tool
+ * wiki directories. Instead, pull.ts has a dedicated wiki tombstone cleanup
+ * block that removes pages from the shared wiki location (~/.teamai/wiki/).
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
@@ -38,10 +43,9 @@ vi.mock('../utils/logger.js', () => ({
import { filterExistingTopLevelPaths } from '../push.js';
import { WikiHandler } from '../resources/wiki.js';
-import { ResourceHandler } from '../resources/base.js';
-import { resolveBaseDir } from '../types.js';
import { remove as removeFile } from '../utils/fs.js';
-import type { TeamaiConfig, LocalConfig, ResourceType } from '../types.js';
+import { getTeamaiHome } from '../types.js';
+import type { TeamaiConfig, LocalConfig } from '../types.js';
// ─────────────────────────────────────────────────────────────────────────
// BUG #1 — filterExistingTopLevelPaths
@@ -107,6 +111,8 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => {
try {
const repoPath = path.join(tmpDir, 'team-repo');
await fse.ensureDir(path.join(repoPath, 'wiki'));
+ const teamaiHome = path.join(tmpDir, '.teamai');
+ await fse.ensureDir(path.join(teamaiHome, 'wiki'));
const teamConfig: TeamaiConfig = {
team: 'test',
@@ -124,7 +130,6 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => {
claude: {
skills: '.claude/skills',
rules: '.claude/rules',
- wiki: '.claude/wiki',
},
},
} as TeamaiConfig;
@@ -133,7 +138,8 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => {
username: 'tester',
updatePolicy: 'auto',
additionalRoles: [],
- scope: 'user',
+ scope: 'project',
+ projectRoot: tmpDir,
} as LocalConfig;
const removed = await handler.removeItem('entities/alpha', teamConfig, localConfig);
@@ -153,18 +159,18 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => {
// ─────────────────────────────────────────────────────────────────────────
describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => {
let tmpDir: string;
- let homeDir: string;
let repoPath: string;
+ let teamaiHome: string;
let teamConfig: TeamaiConfig;
let localConfig: LocalConfig;
beforeEach(async () => {
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-pullts-'));
- homeDir = path.join(tmpDir, 'home');
repoPath = path.join(tmpDir, 'team-repo');
+ teamaiHome = path.join(tmpDir, '.teamai');
+
await fse.ensureDir(path.join(repoPath, 'wiki'));
- await fse.ensureDir(path.join(homeDir, '.claude', 'wiki', 'entities'));
- vi.stubEnv('HOME', homeDir);
+ await fse.ensureDir(path.join(teamaiHome, 'wiki', 'entities'));
teamConfig = {
team: 'test',
@@ -182,7 +188,6 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => {
claude: {
skills: '.claude/skills',
rules: '.claude/rules',
- wiki: '.claude/wiki',
},
},
} as TeamaiConfig;
@@ -192,20 +197,20 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => {
username: 'tester',
updatePolicy: 'auto',
additionalRoles: [],
- scope: 'user',
+ scope: 'project',
+ projectRoot: tmpDir,
} as LocalConfig;
});
afterEach(async () => {
- vi.unstubAllEnvs();
await fse.remove(tmpDir);
});
/**
- * Reproduces the exact loop pull.ts runs after the fix (tombstoneTypes
- * now includes wiki with toolPathField='wiki'). We keep the check at
- * this layer instead of end-to-end because pullForScope touches git,
- * spinners, and file-system fetches that are costly to mock.
+ * Reproduces the new wiki tombstone cleanup logic in pull.ts.
+ * With the refactoring, wiki tombstones are cleaned up from the shared
+ * ~/.teamai/wiki/ location (or /.teamai/wiki/ for project scope)
+ * instead of iterating through per-tool directories.
*/
it('removes local wiki pages that are listed in wiki/.removed', async () => {
// Simulate a team member running `teamai remove wiki entities/alpha`:
@@ -214,36 +219,21 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => {
path.join(repoPath, 'wiki', '.removed'),
'entities/alpha\n',
);
- // Local copy that should be cleaned up.
- const localAlpha = path.join(homeDir, '.claude', 'wiki', 'entities', 'alpha.md');
+ // Local copy in shared wiki location that should be cleaned up.
+ const localAlpha = path.join(teamaiHome, 'wiki', 'entities', 'alpha.md');
await fse.writeFile(localAlpha, '# alpha');
- // Mimic pull.ts tombstone loop with the fixed config
- const tombstoneTypes: {
- type: ResourceType;
- ext?: string;
- toolPathField: 'rules' | 'skills' | 'wiki';
- }[] = [
- { type: 'rules', ext: '.md', toolPathField: 'rules' },
- { type: 'skills', toolPathField: 'skills' },
- { type: 'wiki', ext: '.md', toolPathField: 'wiki' },
- ];
-
+ // Mimic pull.ts wiki tombstone cleanup with the new architecture
const handler = new WikiHandler();
- const baseDir = resolveBaseDir(localConfig);
+ const wikiTombstones = await handler.readTombstones(localConfig);
- for (const { type, ext, toolPathField } of tombstoneTypes) {
- if (type !== 'wiki') continue;
- const tombstones = await handler.readTombstones(localConfig);
- for (const [_tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
- const dir = toolPath[toolPathField];
- if (!dir) continue;
- if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue;
- for (const name of tombstones) {
- const p = path.join(baseDir, dir, ext ? `${name}${ext}` : name);
- if (await fse.pathExists(p)) {
- await removeFile(p);
- }
+ if (wikiTombstones.size > 0) {
+ const sharedWikiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
+ const wikiDir = path.join(sharedWikiHome, 'wiki');
+ for (const name of wikiTombstones) {
+ const wikiPath = path.join(wikiDir, `${name}.md`);
+ if (await fse.pathExists(wikiPath)) {
+ await removeFile(wikiPath);
}
}
}
@@ -256,21 +246,22 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => {
path.join(repoPath, 'wiki', '.removed'),
'entities/alpha\n',
);
- const localAlpha = path.join(homeDir, '.claude', 'wiki', 'entities', 'alpha.md');
- const localBeta = path.join(homeDir, '.claude', 'wiki', 'entities', 'beta.md');
+ const localAlpha = path.join(teamaiHome, 'wiki', 'entities', 'alpha.md');
+ const localBeta = path.join(teamaiHome, 'wiki', 'entities', 'beta.md');
await fse.writeFile(localAlpha, '# alpha');
await fse.writeFile(localBeta, '# beta');
const handler = new WikiHandler();
- const baseDir = resolveBaseDir(localConfig);
- const tombstones = await handler.readTombstones(localConfig);
+ const wikiTombstones = await handler.readTombstones(localConfig);
- for (const [_tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
- if (!toolPath.wiki) continue;
- if (!await ResourceHandler.isToolInstalled(toolPath.wiki, baseDir)) continue;
- for (const name of tombstones) {
- const p = path.join(baseDir, toolPath.wiki, `${name}.md`);
- if (await fse.pathExists(p)) await removeFile(p);
+ if (wikiTombstones.size > 0) {
+ const sharedWikiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
+ const wikiDir = path.join(sharedWikiHome, 'wiki');
+ for (const name of wikiTombstones) {
+ const wikiPath = path.join(wikiDir, `${name}.md`);
+ if (await fse.pathExists(wikiPath)) {
+ await removeFile(wikiPath);
+ }
}
}
diff --git a/src/__tests__/wiki-handler.test.ts b/src/__tests__/wiki-handler.test.ts
index 03c1d70..cd5c63e 100644
--- a/src/__tests__/wiki-handler.test.ts
+++ b/src/__tests__/wiki-handler.test.ts
@@ -19,20 +19,18 @@ import type { TeamaiConfig, LocalConfig } from '../types.js';
describe('WikiHandler', () => {
let tmpDir: string;
- let homeDir: string;
+ let teamaiHome: string;
let handler: WikiHandler;
let teamConfig: TeamaiConfig;
let localConfig: LocalConfig;
beforeEach(async () => {
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-wiki-test-'));
- homeDir = path.join(tmpDir, 'home');
+ teamaiHome = path.join(tmpDir, '.teamai');
const repoPath = path.join(tmpDir, 'team-repo');
await fse.ensureDir(path.join(repoPath, 'wiki'));
- await fse.ensureDir(path.join(homeDir, '.claude-internal', 'wiki'));
-
- vi.stubEnv('HOME', homeDir);
+ await fse.ensureDir(path.join(teamaiHome, 'wiki'));
handler = new WikiHandler();
@@ -49,14 +47,14 @@ describe('WikiHandler', () => {
env: { injectShellProfile: true },
},
toolPaths: {
- 'claude-internal': {
- skills: '.claude-internal/skills',
- rules: '.claude-internal/rules',
- wiki: '.claude-internal/wiki',
+ claude: {
+ skills: '.claude/skills',
+ rules: '.claude/rules',
},
},
};
+ // Use project scope with projectRoot so getTeamaiHome returns the test tmpDir
localConfig = {
repo: {
localPath: repoPath,
@@ -65,19 +63,19 @@ describe('WikiHandler', () => {
username: 'testuser',
updatePolicy: 'auto',
additionalRoles: [],
- scope: 'user',
+ scope: 'project',
+ projectRoot: tmpDir,
};
});
afterEach(async () => {
- vi.unstubAllEnvs();
await fse.remove(tmpDir);
});
describe('scanLocalForPush', () => {
it('detects new wiki page not in team repo', async () => {
- // Create local wiki page
- const localWiki = path.join(homeDir, '.claude-internal', 'wiki');
+ // Create local wiki page in shared location
+ const localWiki = path.join(teamaiHome, 'wiki');
await fse.ensureDir(path.join(localWiki, 'entities'));
await fse.writeFile(
path.join(localWiki, 'entities', 'test-module.md'),
@@ -102,7 +100,7 @@ describe('WikiHandler', () => {
);
// Create same page locally with different content
- const localWiki = path.join(homeDir, '.claude-internal', 'wiki');
+ const localWiki = path.join(teamaiHome, 'wiki');
await fse.ensureDir(path.join(localWiki, 'entities'));
await fse.writeFile(
path.join(localWiki, 'entities', 'test-module.md'),
@@ -124,7 +122,7 @@ describe('WikiHandler', () => {
await fse.ensureDir(path.join(teamWiki, 'entities'));
await fse.writeFile(path.join(teamWiki, 'entities', 'test-module.md'), content);
- const localWiki = path.join(homeDir, '.claude-internal', 'wiki');
+ const localWiki = path.join(teamaiHome, 'wiki');
await fse.ensureDir(path.join(localWiki, 'entities'));
await fse.writeFile(path.join(localWiki, 'entities', 'test-module.md'), content);
@@ -134,7 +132,7 @@ describe('WikiHandler', () => {
});
it('excludes _metadata.json from push', async () => {
- const localWiki = path.join(homeDir, '.claude-internal', 'wiki');
+ const localWiki = path.join(teamaiHome, 'wiki');
await fse.writeFile(
path.join(localWiki, '_metadata.json'),
'{"version":1}',
@@ -185,7 +183,7 @@ describe('WikiHandler', () => {
describe('pushItem', () => {
it('copies wiki page to team repo', async () => {
- const localWiki = path.join(homeDir, '.claude-internal', 'wiki');
+ const localWiki = path.join(teamaiHome, 'wiki');
await fse.ensureDir(path.join(localWiki, 'entities'));
const sourcePath = path.join(localWiki, 'entities', 'test.md');
await fse.writeFile(sourcePath, '# Test');
@@ -209,7 +207,7 @@ describe('WikiHandler', () => {
});
describe('pullItem', () => {
- it('copies wiki page to local tool directory', async () => {
+ it('copies wiki page to shared wiki location', async () => {
const teamWiki = path.join(localConfig.repo.localPath, 'wiki');
await fse.ensureDir(path.join(teamWiki, 'entities'));
const sourcePath = path.join(teamWiki, 'entities', 'test.md');
@@ -226,7 +224,7 @@ describe('WikiHandler', () => {
localConfig,
);
- const dest = path.join(homeDir, '.claude-internal', 'wiki', 'entities', 'test.md');
+ const dest = path.join(teamaiHome, 'wiki', 'entities', 'test.md');
expect(await fse.pathExists(dest)).toBe(true);
expect(await fse.readFile(dest, 'utf-8')).toBe('# Test from team');
});
@@ -234,7 +232,7 @@ describe('WikiHandler', () => {
describe('rebuildMetadata', () => {
it('rebuilds metadata from wiki pages', async () => {
- const wikiDir = path.join(tmpDir, 'test-wiki');
+ const wikiDir = path.join(teamaiHome, 'wiki');
await fse.ensureDir(path.join(wikiDir, 'entities'));
await fse.writeFile(
path.join(wikiDir, 'entities', 'foo.md'),
@@ -245,7 +243,7 @@ describe('WikiHandler', () => {
'---\ntitle: Bar\ncategory: entity\ntags: [util]\nupdated: 2026-04-08\n---\n\n# Bar\n\n## Related\n\n## Backlinks\n',
);
- await WikiHandler.rebuildMetadata(wikiDir);
+ await WikiHandler.rebuildMetadata(localConfig);
const metadataPath = path.join(wikiDir, '_metadata.json');
expect(await fse.pathExists(metadataPath)).toBe(true);
diff --git a/src/pull.ts b/src/pull.ts
index ea004b4..259c228 100644
--- a/src/pull.ts
+++ b/src/pull.ts
@@ -417,11 +417,10 @@ async function pullForScope(
const tombstoneTypes: {
type: ResourceType;
ext?: string;
- toolPathField: 'rules' | 'skills' | 'wiki';
+ toolPathField: 'rules' | 'skills';
}[] = [
{ type: 'rules', ext: '.md', toolPathField: 'rules' },
{ type: 'skills', toolPathField: 'skills' },
- { type: 'wiki', ext: '.md', toolPathField: 'wiki' },
];
const baseDir = resolveBaseDir(localConfig);
@@ -445,6 +444,25 @@ async function pullForScope(
}
}
+ // Wiki tombstone cleanup: wiki is now in shared location, not per-tool
+ try {
+ const wikiHandler = getHandler('wiki');
+ const wikiTombstones = await wikiHandler.readTombstones(localConfig);
+ if (wikiTombstones.size > 0) {
+ const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
+ const wikiDir = path.join(teamaiHome, 'wiki');
+ for (const name of wikiTombstones) {
+ const wikiPath = path.join(wikiDir, `${name}.md`);
+ if (await pathExists(wikiPath)) {
+ await remove(wikiPath);
+ log.debug(`[${scopeLabel}] Cleaned up tombstoned wiki ${name} from shared wiki`);
+ }
+ }
+ }
+ } catch (e) {
+ log.debug(`[${scopeLabel}] Wiki tombstone cleanup skipped: ${(e as Error).message}`);
+ }
+
if (roleContext) {
await cleanupInactiveNamespaceSkills(
freshConfig,
diff --git a/src/resources/wiki.ts b/src/resources/wiki.ts
index 43df0d8..3973f85 100644
--- a/src/resources/wiki.ts
+++ b/src/resources/wiki.ts
@@ -1,7 +1,7 @@
import path from 'node:path';
import { ResourceHandler } from './base.js';
import type { ResourceItem, ResourceItemStatus, TeamaiConfig, LocalConfig } from '../types.js';
-import { resolveBaseDir } from '../types.js';
+import { resolveBaseDir, getTeamaiHome } from '../types.js';
import {
listFilesRecursive,
pathExists,
@@ -9,7 +9,6 @@ import {
remove,
ensureDir,
readFileSafe,
- getFileMtime,
fileContentEqual,
} from '../utils/fs.js';
import { log } from '../utils/logger.js';
@@ -18,13 +17,20 @@ import { log } from '../utils/logger.js';
/**
* Wiki resource handler.
*
- * Unlike skills (directory-based), wiki pages are individual .md files
- * stored in category subdirectories (entities/, concepts/, etc.).
- * The handler treats the entire wiki/ tree as flat files keyed by
- * their relative path within wiki/ (e.g. "entities/message-builder").
+ * Wiki pages are now stored in a centralized shared location:
+ * - User scope → ~/.teamai/wiki/
+ * - Project scope → /.teamai/wiki/
+ *
+ * Individual pages are stored as .md files in category subdirectories
+ * (entities/, concepts/, etc.). The handler treats the entire wiki/ tree
+ * as flat files keyed by their relative path within wiki/.
*
* _metadata.json is NOT synced via push/pull — it is rebuilt locally
* by the /wiki skill after each pull.
+ *
+ * BREAKING CHANGE: Wiki is no longer distributed to individual tool
+ * directories (e.g., ~/.claude/wiki/, ~/.cursor/wiki/). All tools now
+ * reference the single shared wiki at ~/.teamai/wiki/.
*/
export class WikiHandler extends ResourceHandler {
readonly type = 'wiki' as const;
@@ -55,7 +61,17 @@ export class WikiHandler extends ResourceHandler {
}
/**
- * Scan local AI tool wiki directories for pages that are new or modified
+ * Get the shared wiki directory for the current scope.
+ * - User scope → ~/.teamai/wiki/
+ * - Project scope → /.teamai/wiki/
+ */
+ private static getSharedWikiDir(localConfig: LocalConfig): string {
+ const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
+ return path.join(teamaiHome, 'wiki');
+ }
+
+ /**
+ * Scan the shared wiki directory for pages that are new or modified
* compared to the team repo.
*/
async scanLocalForPush(
@@ -76,76 +92,44 @@ export class WikiHandler extends ResourceHandler {
}
}
- const tombstones = await this.readTombstones(localConfig);
+ const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig);
+ if (!await pathExists(sharedWikiDir)) return [];
- // Collect best candidate for each page across all tool directories
- const candidates = new Map<
- string,
- { sourcePath: string; mtime: number; status: ResourceItemStatus }
- >();
+ const tombstones = await this.readTombstones(localConfig);
+ const items: ResourceItem[] = [];
- for (const [__, toolPath] of Object.entries(teamConfig.toolPaths)) {
- if (!toolPath.wiki) continue;
- const wikiDir = path.join(resolveBaseDir(localConfig), toolPath.wiki);
- if (!await pathExists(wikiDir)) continue;
+ const files = await listFilesRecursive(sharedWikiDir);
+ for (const file of files) {
+ if (!WikiHandler.isWikiPage(file)) continue;
- const files = await listFilesRecursive(wikiDir);
- for (const file of files) {
- if (!WikiHandler.isWikiPage(file)) continue;
-
- const name = WikiHandler.pathToName(file);
- if (tombstones.has(name)) continue;
-
- const localFilePath = path.join(wikiDir, file);
-
- if (teamPages.has(name)) {
- const teamFilePath = teamPages.get(name)!;
- const equal = await fileContentEqual(localFilePath, teamFilePath);
- if (equal) continue;
-
- const mtime = await getFileMtime(localFilePath);
- const existing = candidates.get(name);
- if (!existing || mtime > existing.mtime) {
- candidates.set(name, {
- sourcePath: localFilePath,
- mtime,
- status: 'modified',
- });
- }
- } else {
- const existing = candidates.get(name);
- if (!existing) {
- const mtime = await getFileMtime(localFilePath);
- candidates.set(name, {
- sourcePath: localFilePath,
- mtime,
- status: 'new',
- });
- } else if (existing.status === 'new') {
- const mtime = await getFileMtime(localFilePath);
- if (mtime > existing.mtime) {
- candidates.set(name, {
- sourcePath: localFilePath,
- mtime,
- status: 'new',
- });
- }
- }
- }
+ const name = WikiHandler.pathToName(file);
+ if (tombstones.has(name)) continue;
+
+ const localFilePath = path.join(sharedWikiDir, file);
+
+ if (teamPages.has(name)) {
+ const teamFilePath = teamPages.get(name)!;
+ const equal = await fileContentEqual(localFilePath, teamFilePath);
+ if (equal) continue;
+
+ items.push({
+ name,
+ type: 'wiki',
+ sourcePath: localFilePath,
+ relativePath: `wiki/${name}.md`,
+ status: 'modified',
+ });
+ } else {
+ items.push({
+ name,
+ type: 'wiki',
+ sourcePath: localFilePath,
+ relativePath: `wiki/${name}.md`,
+ status: 'new',
+ });
}
}
- const items: ResourceItem[] = [];
- for (const [name, candidate] of candidates) {
- items.push({
- name,
- type: 'wiki',
- sourcePath: candidate.sourcePath,
- relativePath: `wiki/${name}.md`,
- status: candidate.status,
- });
- }
-
return items;
}
@@ -185,50 +169,38 @@ export class WikiHandler extends ResourceHandler {
}
/**
- * Pull a wiki page from team repo to all configured AI tool wiki directories.
+ * Pull a wiki page from team repo to the shared wiki directory.
*/
async pullItem(
item: ResourceItem,
- teamConfig: TeamaiConfig,
+ _teamConfig: TeamaiConfig,
localConfig: LocalConfig,
): Promise {
- const baseDir = resolveBaseDir(localConfig);
-
- for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
- if (!toolPath.wiki) continue;
-
- if (!await ResourceHandler.isToolInstalled(toolPath.wiki, baseDir)) {
- log.debug(`Skipping wiki sync for ${tool}: tool not installed`);
- continue;
- }
-
- const dest = path.join(baseDir, toolPath.wiki, `${item.name}.md`);
- await ensureDir(path.dirname(dest));
- try {
- await copyFile(item.sourcePath, dest);
- log.debug(`Synced wiki page ${item.name} → ${tool}`);
- } catch (e) {
- log.warn(
- `Failed to sync wiki page ${item.name} to ${tool}: ${(e as Error).message}`,
- );
- }
+ const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig);
+ const dest = path.join(sharedWikiDir, `${item.name}.md`);
+ await ensureDir(path.dirname(dest));
+ try {
+ await copyFile(item.sourcePath, dest);
+ log.debug(`Synced wiki page ${item.name} → shared wiki`);
+ } catch (e) {
+ log.warn(
+ `Failed to sync wiki page ${item.name}: ${(e as Error).message}`,
+ );
}
}
/**
- * Remove a wiki page from the team repo and all local AI tool wiki directories.
+ * Remove a wiki page from the team repo and the shared wiki directory.
*/
async removeItem(
name: string,
- teamConfig: TeamaiConfig,
+ _teamConfig: TeamaiConfig,
localConfig: LocalConfig,
): Promise {
const removed: string[] = [];
- const baseDir = resolveBaseDir(localConfig);
- const fileName = `${name}.md`;
// Remove from team repo
- const teamFile = path.join(localConfig.repo.localPath, 'wiki', fileName);
+ const teamFile = path.join(localConfig.repo.localPath, 'wiki', `${name}.md`);
if (await pathExists(teamFile)) {
await remove(teamFile);
removed.push(teamFile);
@@ -236,25 +208,24 @@ export class WikiHandler extends ResourceHandler {
await this.addTombstone(name, localConfig);
- // Remove from each tool's wiki directory
- for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
- if (!toolPath.wiki) continue;
- const filePath = path.join(baseDir, toolPath.wiki, fileName);
- if (await pathExists(filePath)) {
- await remove(filePath);
- removed.push(filePath);
- log.debug(`Removed wiki page ${name} from ${tool}`);
- }
+ // Remove from shared wiki directory
+ const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig);
+ const sharedFile = path.join(sharedWikiDir, `${name}.md`);
+ if (await pathExists(sharedFile)) {
+ await remove(sharedFile);
+ removed.push(sharedFile);
+ log.debug(`Removed wiki page ${name} from shared wiki`);
}
return removed;
}
/**
- * Rebuild _metadata.json from wiki pages on disk.
+ * Rebuild _metadata.json from wiki pages in the shared wiki directory.
* Called after pull to reconstruct local metadata.
*/
- static async rebuildMetadata(wikiDir: string): Promise {
+ static async rebuildMetadata(localConfig: LocalConfig): Promise {
+ const wikiDir = WikiHandler.getSharedWikiDir(localConfig);
if (!await pathExists(wikiDir)) return;
const files = await listFilesRecursive(wikiDir);
diff --git a/src/utils/fs.ts b/src/utils/fs.ts
index 6243f08..ff44fac 100644
--- a/src/utils/fs.ts
+++ b/src/utils/fs.ts
@@ -8,6 +8,7 @@ const IGNORED_NAMES = new Set([
'.pyc',
'.DS_Store',
'node_modules',
+ '.git',
]);
function isIgnored(name: string): boolean {
diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts
index 5308c21..2f1f69e 100644
--- a/vitest.e2e.config.ts
+++ b/vitest.e2e.config.ts
@@ -8,10 +8,12 @@ export default defineConfig({
],
testTimeout: 60_000,
hookTimeout: 30_000,
- // E2E tests spawn child processes and touch the real filesystem; a few
- // (notably auto-recall-e2e) are inherently slightly flaky on CI runners
- // due to timing/state. Retry once: flaky tests recover, real bugs stay
- // failed (since they'll fail both runs).
+ // E2E tests spawn child processes and touch the real filesystem.
+ // Run test files sequentially to avoid race conditions (parallel
+ // file-level execution causes intermittent "Cannot find module
+ // dist/index.js" on GitHub Actions CI runners).
+ fileParallelism: false,
+ // Retry once: flaky tests recover, real bugs stay failed.
retry: 1,
},
});