diff --git a/AGENTS.md b/AGENTS.md index 7a9df7b0..d159c371 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,9 @@ TypeScript, Node 20+, tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc -- - CLI user-facing output must be English. No Chinese in production code. Tests assert English output. - Keep bilingual docs in sync (`README` / `*.zh-CN.md`, `docs/usage-guide.*`). Behavior changes must update every affected doc (including `docs/designs/`); grep old wording before opening the PR. - **README 精简**:尽量少改动 README,保持简洁。确需改动时,所有语言版本(`README.md` 及全部 `README.*.md`,改前先 `ls README*` 确认清单)必须全部改完并保持一致。 +- **`skill-data/` 与文档同等对待**:那是 agent 真正读到的内容。行为变更必须同步更新受影响的 skill(`core` / `setup` / `wiki` / `share`),并在 PR 前 grep 旧措辞。 +- `skill-data/core/references/commands.md` 由 Commander 命令表生成,改动命令或 flag 后运行 `npx vitest run commands-reference -u` 重新生成。 +- 部署到 agent 的只有 `skills/teamai/SKILL.md`(发现入口),保持与版本无关:新增工作流是在 `skill-data/` 下加目录 + 在 stub 里加一行,不要把内容写进 stub。 - **奥卡姆剃刀**:避免过早添加新 CLI 命令;非必要不加;优先复用或扩展现有命令与选项。 ## PR 前测试 diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b7d2e3..7594c6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file. See [standa ### ✨ Features +- Built-in skill content ships inside the npm package and is printed by the installed CLI: `teamai skill get [--full] [--all]`, `teamai skill path ` for the directory holding a skill's scripts, and `teamai skill list --json` for the catalog. Agents receive one file, `skills/teamai/SKILL.md`, a discovery stub that points at those commands, so what an agent reads always matches the CLI version it is running. `teamai pull` removes the `team-wiki-codebase`, `teamai-share-learnings` and `teamai/references/*.md` trees earlier releases copied into every agent directory, removing only files whose content a release shipped (an edited file, or a member's own skill under an old name, stays), archiving each removed file under `~/.teamai/removed-skills//…` first, and keeping any directory that holds a member's own file; `teamai uninstall` removes only the packaged files from CLI-owned skill directories by the same rule. `share` is served only while recall is on and the team source is writable (not a read-only HTTP one), and the end-of-session share reminder is withheld until then too. The served workflows are English; learning and knowledge-base documents are still written in Simplified Chinese, and an existing knowledge base keeps its file names and headings. The legacy names still resolve as aliases (for [#678](https://github.com/Tencent/teamai-cli/issues/678), [#730](https://github.com/Tencent/teamai-cli/issues/730)). + - Hooks, MCP servers and env variables can be scoped by logical project, the second membership axis they lacked. A `hooks/hooks.yaml` hook and an `mcp/mcp.yaml` server accept an optional `projects:` list beside `roles:`, and an `env/env.yaml` variable accepts both. An entry reaches a member when one of the projects its directory is bound to (`teamai projects set`) is listed; `projects: []` reaches nobody, and a directory bound to no project keeps receiving every entry, so nothing changes until a maintainer adds the key. The two axes compose as AND, the way `tools:` and `roles:` already do, so `roles: [frontend] projects: [checkout]` reaches frontend members of checkout rather than everyone on either. Rebinding with `teamai projects set` removes the previous project's entries on the next pull — for env that means the variable leaves `env.sh`, also on a pull that finds the team repo unchanged, so a machine upgrading from a CLI that ignored the keys drops a withheld variable without `--force`, and a refresh that cannot be written there is reported with the path and the way out rather than passing silently under `Already synced`; `teamai doctor` applies the same filter, so a variable correctly withheld is not reported as undelivered, while one that `env.sh` still exports after a rebind is reported until the next pull rewrites the file. An id that `manifest/projects.yaml` does not define produces one warning per pull, and so does a `projects:` key in a team with no projects manifest: the key still filters against the ids in the directory's `config.yaml`, but nothing can validate them. `teamai mcp list`, `teamai hooks list` and `teamai env list` show the restriction, and `pull` reports `Synced 1 of 3 env variable(s)` when scoping withheld some. This is what the keys exist to control: a team with five projects and three MCP servers each gave every member of a role fifteen server processes and fifteen tool lists in the context of every session (for [#668](https://github.com/Tencent/teamai-cli/issues/668)). - `teamai doctor` now checks what landed for every resource, not only skills and docs. `Rules delivered to ` and `Agents delivered to ` ask the resource handler where an item lands — a rule's filename and content change per tool, an agent's destination comes from its render and its `targets:` — and compare a delivered rule with the bytes the handler renders for that tool, so a `.mdc` whose `globs` drifted from the team rule's `paths:` is reported rather than passing on the presence of its frontmatter keys. An agent is compared with the bytes its render produces, so a copy left behind by an older spec is reported rather than counted as delivered. `Every team agent reaches a tool` names an agent that renders for no installed tool, and is reported whenever a tool is installed to receive agents, including when no agent renders anywhere. Two tools do not read a rules directory and get a check each: `Team rules are active in opencode` fails when `opencode.json` stops listing the glob that makes the delivered `.md` files load at all, and `Team rules are inlined in Hermes SOUL.md` compares the managed block of `SOUL.md` with what the team rules inline to. `MCP servers delivered to ` compares each server the team resolves for a tool with the entry in that tool's own config — the entry, not the name, since reconciliation leaves an entry teamai does not own alone, so an unrelated server under a team name holds the key while the team's definition never arrives — and names any the reconcile skipped with its reason, so an unresolved `${VAR}` is reported with the variable instead of being mentioned once during a pull and never again. An `mcp.yaml` that does not parse is reported as `Team MCP servers can be read` rather than read as a team shipping no MCP at all. `Env variables injected in shell profile` stops at the marker comment no longer: it checks that `env/env.yaml` parses and declares its variables under `variables:` (an explicit `variables: []` is an empty configuration and fails nothing), that each reached `env.sh` with the declared value — read back through the generator's own inverse, so a multiline value quoted across several lines is matched rather than reported stale — and that the injected block would actually load it. The two expensive registries, rules and agents, are built for `teamai doctor` only, so the checks at the end of a pull keep their budget (for [#624](https://github.com/Tencent/teamai-cli/issues/624)). diff --git a/CLAUDE.md b/CLAUDE.md index 6f21a2fd..e93ec1fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,9 @@ TypeScript, Node 20+, tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc -- - CLI user-facing output must be English. No Chinese in production code. Tests assert English output. - Keep bilingual docs in sync (`README` / `*.zh-CN.md`, `docs/usage-guide.*`). Behavior changes must update every affected doc (including `docs/designs/`); grep old wording before opening the PR. - **README 精简**:尽量少改动 README,保持简洁。确需改动时,所有语言版本(`README.md` 及全部 `README.*.md`,改前先 `ls README*` 确认清单)必须全部改完并保持一致。 +- **`skill-data/` 与文档同等对待**:那是 agent 真正读到的内容。行为变更必须同步更新受影响的 skill(`core` / `setup` / `wiki` / `share`),并在 PR 前 grep 旧措辞。 +- `skill-data/core/references/commands.md` 由 Commander 命令表生成,改动命令或 flag 后运行 `npx vitest run commands-reference -u` 重新生成。 +- 部署到 agent 的只有 `skills/teamai/SKILL.md`(发现入口),保持与版本无关:新增工作流是在 `skill-data/` 下加目录 + 在 stub 里加一行,不要把内容写进 stub。 - **奥卡姆剃刀**:避免过早添加新 CLI 命令;非必要不加;优先复用或扩展现有命令与选项。 ## PR 前测试 diff --git a/docs/designs/git-native-memory.md b/docs/designs/git-native-memory.md index de2b2945..d9b9d1fe 100644 --- a/docs/designs/git-native-memory.md +++ b/docs/designs/git-native-memory.md @@ -75,7 +75,7 @@ 1. `src/types.ts` — LearningDoc, SearchIndex types 2. `src/utils/search-index.ts` — buildIndex(), loadIndex(), search() with Intl.Segmenter 3. `src/pull.ts` — syncLearnings() step + index rebuild -4. `skills/teamai-share-learnings/SKILL.md` — frontmatter 标准化 +4. `skill-data/share/SKILL.md` — frontmatter 标准化(由 `teamai skill get share` 提供,不再部署到各 agent) 5. Tests: index build, search, CJK, edge cases ### Phase 2: Recall + Voting diff --git a/docs/designs/skill-serving.md b/docs/designs/skill-serving.md new file mode 100644 index 00000000..7888ca16 --- /dev/null +++ b/docs/designs/skill-serving.md @@ -0,0 +1,241 @@ +# Serving built-in skill content from the CLI + +Issue: [#678](https://github.com/Tencent/teamai-cli/issues/678). Unreleased; targets the release after 0.25.0. + +## The problem + +The built-in skills describe the CLI, but they did not travel with it. +`deployBuiltinSkills` copied three whole skill directories into every installed +agent's skills directory on `init`, on `pull` and on a recall toggle, and nothing +else touched them. After `npm i -g teamai-cli@latest` the agent kept reading the +previous release's instructions until the member happened to run a pull, and a +machine with several agents could hold several different versions at once. Four +commits exist only to re-align deployed text after a command changed (`e151d43`, +`1ca43ac`, `8bb0548`, `2ddb546`), and every one of them needed a pull on every +machine to take effect. + +The copies were also large — 176 KB per agent, with +`skills/team-wiki-codebase/SKILL.md` alone at 38 705 bytes read in full on every +activation — but that is the secondary cost. The primary one is that the agent's +instructions and the binary they describe were versioned separately. + +## The shape + +**The skill content is versioned with the CLI.** It ships inside the npm package +and is printed by the installed binary, so `teamai skill get core` on version X +prints version X's instructions, byte for byte, with no pull in between. +Upgrading the CLI is the update; there is nothing else to sync. + +One deployable unit, everything else served on demand. The pattern is +`vercel-labs/agent-browser`'s, verified against its published 0.38.1 package. + +```text +npm package +├── skills/ +│ └── teamai/SKILL.md the only unit deployed into agents (~2 KB) +└── skill-data/ never deployed; printed by `teamai skill get` + ├── core/ daily sync, routing, publishing a skill, command reference + ├── setup/ day 0 and repo lifecycle + ├── wiki/ codebase knowledge base, incl. scripts/ + └── share/ session learnings +``` + +`skills/` keeps the invariant "everything here is deployed", which is what lets +`BUILTIN_SKILL_NAMES` hold a single name instead of a list of guards. + +What an agent reads, and when: + +```text +session start stub frontmatter (description) 1 005 B always in context +task matches stub body 1 524 B holds the commands +`teamai skill get core` daily workflow 6 479 B on demand +`… core --full` + commands.md, contribute-member, + troubleshooting 36 267 B on demand +`… setup` / `wiki` 5 566 B / 19 315 B on demand +`… setup --full` / `… wiki --full` 38 570 B / 132 949 B on demand +``` + +Served sizes include the resolved `{SKILL_DIR}`, so they grow with the install +path (measured here from a 77-character one). + +## Contracts worth keeping + +- **`skill get` prints the file byte for byte**, frontmatter included, with no + banner. The only transformation is `{SKILL_DIR}`, replaced with the absolute + packaged directory, so a documented `python3 {SKILL_DIR}/scripts/scan_repo.py` + runs as written. `agent-browser` leaves that placeholder unsubstituted; an + agent copying such a line literally fails, which is why we resolve it. +- **Content on stdout, diagnostics on stderr.** An unknown flag warns and the + command continues — a hallucinated flag should not cost a round trip. An + unknown *name* is fatal: acting on the wrong instructions is worse than a + retry. +- **`--full` walks `references/` and `templates/` recursively**, sorted by + relative path. Our references nest (`references/methodology/`, + `references/phases/`); a single-level scan would serve an incomplete skill. +- **The stub lands where team skills land.** Deploy, the legacy prune, + `recall disable` and `uninstall` resolve the skills directory through + `skillsDirForTool`, the resolver team-skill sync uses, so OpenClaw gets it in + its workspace and Hermes under `HERMES_HOME` rather than under a tool root + that agent never reads. The link guard walks from the scope root (home, or + the project root) when the skills directory is under it, else from just above + the configured root, so that root is checked too. +- **Nothing repairs the deployed stub.** `ensureSkillFrontmatter` is not called + on it, so deployed and packaged bytes are identical and a diff means a bug. +- **Recall is decided at run time**, not by withholding a directory at deploy + time, and so is the read-only HTTP source that `reportingOnly` used to skip + `share` for (`teamai contribute` refuses there, so the workflow would fail at + its last step; `skill list --json` reports `blockedBy: "read-only"`). Both hold + on every path that hands out content or a location: + `skill get ` refuses, `skill get --all` leaves the skill out and says so + on stderr, `skill path ` and `skill show ` refuse, and + `skill list --json` reports `blockedBy: "recall"` with `path: null`. With no + config on the machine at all it fails open: a refusal a fresh install cannot act + on is worse than serving the workflow. A config that exists but cannot be loaded + blocks instead (`blockedBy: "config"`), since recall and the source are then + unknown and the workflow would fail at `teamai contribute` — a project config + too, which detection alone would skip in favour of the user config + (`findUnreadableProjectConfig`). The Stop-hook reminder follows the same rule. The Stop-hook share + reminder is gated the same way (`contributeHintAllowed`, `src/hook-handlers.ts`), + because it points at this command. The gate lives in one place: + `resolveServableSkill` (`src/skill-content.ts`) is the only way to obtain a + packaged skill outside that module, and it returns `blocked` instead of the + skill, so a command cannot print a directory it never received. +- **`skill path` takes a name, always,** and a blocked name gets the same + refusal as `skill get`. The gate routes the agent away from a workflow that + cannot finish; it is not access control, since the files ship in the package. +- **A member's own skill outranks a packaged name.** `locateSkill` searches the + team repo, then the installed agents, then the package. `codebase`, `default`, + `learning` and `share` are ordinary names: a directory a member created under + one of them is the skill they are asking about, and the recall gate does not + apply to it. The two legacy directory names are the exception, by design: + `team-wiki-codebase` and `teamai-share-learnings` classify as `[builtin]` and + are skipped by the push scan by name alone (`isCliOwnedSkillName`), because a + tree with that name is one a pre-stub release wrote until the first pull has + pruned it. That rule retires with `LEGACY_BUILTIN_SKILL_NAMES`. +- **`skill get` has no `--json`.** #678 sketched one; the content is markdown for + an agent to read, and the machine-readable half is `skill list --json`. An + unknown flag on `skill get`, `--json` included, is warned about on stderr and + ignored, so the content still arrives. +- **`skill list` needs no team.** The human-readable listing prints the packaged + catalog even before `teamai init`, with a hint for the team half, so a fresh + machine can discover what the installed CLI serves the way `skill get` lets it. + + +## Drift guards + +Two tests, both in the unit suite: + +- `commands-reference.test.ts` renders `skill-data/core/references/commands.md` + from the Commander table and diffs it. Regenerate with + `npx vitest run commands-reference -u`. +- `skill-commands-exist.test.ts` resolves every `teamai …` string written + anywhere under `skill-data/` against that same table, and fails on an unknown + command or flag. It carries a case proving it catches `teamai extract graph`, + the command the wiki skill advertised for four releases. + +A third, in `skill-content.test.ts`, asserts through `npm pack` that both +`skills/` and `skill-data/` are in the published tarball. Without it, a missing +`package.json` "files" entry passes every other test and serves nothing once +installed. The same file fails on Chinese text under `skills/` or `skill-data/`: +both reach the agent as CLI output, which the repo keeps English. + +## Migration + +`LEGACY_BUILTIN_SKILL_NAMES` (`src/builtin-skills.ts`) names the directories +earlier releases deployed: `team-wiki-codebase` and `teamai-share-learnings`. +Deployment removes those two, after the stub is in place, from every installed, +non-excluded agent, in its configured skills path; Codex's pass also covers the +shared `.agents/skills`, which no other tool's pass touches. `teamai-workflow` +and `teamai-import` sat in the old guard set but were never packaged, so they +are not in it: a directory by either name is the user's own and is never +touched. + +Codex's shared root is on the removal side of three commands now, because +`resolveSkillDestination` puts the stub there whenever the skill already lives +there: the legacy prune, `recall disable`, and `uninstall`, whose skill discovery +adds `.agents/skills` for `codex` alone. Without it, an uninstall reported +success while leaving `~/.agents/skills/teamai` behind. + +**`uninstall` deletes a CLI-owned directory by the same rule.** A team-repo skill +is synced whole, so uninstall removes the whole directory. A CLI-owned one is +not: deployment writes only `PACKAGED_SKILL_FILES` and never touched a file the +member added beside them, so uninstall removes those same paths through +`removeOwnedFiles` and keeps the rest, saying which directory it kept. Deleting +the directory there would undo, one command over, the guarantee pull makes. + +Pull's archive is deliberately not applied there: pull runs on an upgrade the +member did not ask anything to be removed by, while uninstall is them asking for +all of it to go. Leaving copies behind would be the thing they ran it to avoid. + +**It removes only the files those releases packaged, at the content they +packaged.** `PACKAGED_SKILL_DIGESTS` (`src/packaged-skill-digests.ts`) records the +sha256 of every blob `git ls-tree -r -- skills/` shows over all 100 tags +through v0.25.0 and `main` before the stub, minus `teamai-wiki` (see below): 42 +versions across 21 paths. A file is ours only at one of those paths *and* with one +of those digests, whole file, frontmatter included: a member who changed only a +skill's description changed the skill. The deploy repaired frontmatter from +0.16.1 on, but every `SKILL.md` those releases shipped was already complete, and +the copies came from the npm tarball byte for byte, so an unedited one matches. +No release shipped a symlink, so a link is never ours, and bytecode is ours only +beside a script proven ours by content. Anything else at a packaged path — an edit, a +member's own skill that uses a legacy name, a root TeamAI never managed because +`toolPaths` or `HERMES_HOME` moved — is the member's and stays, with its +directory. Checking the path alone would have deleted those. What is removed is +still copied first to +`~/.teamai/removed-skills//////`, so no +removal is a one-way door. Order matters as much as ownership: the stub is +written first, then the references it no longer points at are pruned, and the +legacy trees go only once the stub deployed for that agent, so a stub that cannot +be written leaves a working old skill rather than a broken one. The destination +is resolved without side effects before the link guard runs. Codex reads both `.codex/skills` and the shared `.agents/skills`, and the +stub goes to the shared one when a copy already lives there; the copy an earlier +release left in the other root is retired by the same rule +(`retireOtherCodexCopy`), so Codex never sees a stale `teamai` beside the current +one. That path is the machine's +home, never the tool's base directory, which under project scope is the repo +root. Only *retired* paths are archived: the stub is rewritten on every session +start, so archiving it would file an identical copy per session forever. A +link on any component between the scope root (home, or the project root) and +the skill directory — `~/.claude`, `~/.config/opencode`, `~/.claude/skills`, +`COPILOT_HOME`, the skill directory itself — is refused outright: neither pruned nor written through, link +and target untouched, since everything under it matches our names and none of it +is ours. Pull, deploy and `uninstall` apply the same check; uninstall carries +each skill directory's base for it. Components at or above the base are not +checked: a home directory under a link is ordinary. The cost is a member whose +whole `~/.claude` is a link (stow, chezmoi): the stub is not deployed and the +legacy trees stay, with a warning on each pull naming the path, until the link +is replaced by a directory. Deleting through a link is the one thing the prune +must never do, so that member is told rather than guessed for. The +`` segment is there because `inheritUserScope` deploys the user base and +then the project base in one process, with the same tool, root and skill name. A file whose copy fails is +kept rather than removed: a backup that did not happen must not authorise the +delete. The path carries the run and the skill root because neither is unique on +its own — two pulls land on the same day, and Codex prunes the same skill name +from both `.codex/skills` and the shared `.agents/skills`. Directories left empty go; a directory still holding a member's +file is kept, and `pull` says which one and why. Python bytecode of a script we +shipped counts as ours, so a `__pycache__` left by running the wiki scripts does +not strand the tree. The same rule governs the stub directory: the seven +`teamai/references/*.md` a pre-stub release wrote are removed by name, not by +"everything that is not SKILL.md". + +`teamai-wiki` (0.13.0, 0.16.x) is deliberately not in the set. It predates the +trees this migration is about, and widening a destructive set belongs in its own +change. + +Between the upgrade and that first pull the legacy trees are still on disk, so +two other commands know the names too: `push` never offers them as new user +skills (`isCliOwnedSkillName`), and `recall disable` still removes +`teamai-share-learnings` (`LEGACY_RECALL_SKILL_NAMES`), as it did before the stub. + +**Retire that set once 0.25.x, the last release to deploy those trees, is no longer in the field.** The short names +(`wiki`, `share`) are the canonical ones; the long names survive as aliases in +`SKILL_ALIASES` (`src/skill-content.ts`) for documentation and muscle memory, +and can be dropped on the same schedule. + +`/teamai-share-learnings` was never a deployed slash command in its own right — +it existed because the directory was installed. The Stop-hook nudge now names +`/teamai share what this session taught me`, an invocation the core skill routes +to `share` (bare `/teamai` prints the menu and stops), and carries +`teamai skill get share` literally, so an agent can act on it even without +inferring the intent. It is withheld while recall is off, because that command +refuses then. diff --git a/docs/product-overview.md b/docs/product-overview.md index ac33bec1..6e1ead9b 100644 --- a/docs/product-overview.md +++ b/docs/product-overview.md @@ -115,10 +115,10 @@ When a session ends, the Stop hook scores it by **friction** — signals that th Task: Fix duplicate project-level Hook injection -Consider running /teamai-share-learnings to summarize what you learned and share it with your team. +Consider running `/teamai share what this session taught me` to summarize what you learned and share it with your team (or run `teamai skill get share`). ``` -The hint names the non-zero friction signals that triggered it and, when available, includes a redacted, single-line summary of the first task. The `/teamai-share-learnings` skill summarizes the session and pushes a learning document directly to the team repo. Each session is prompted at most once. Teams can switch the hint off with `sharing.contributeHint.enabled: false` in `teamai.yaml` (members: `contributeHintEnabled` in local config) while keeping the rest of the Stop hook. +The hint names the non-zero friction signals that triggered it and, when available, includes a redacted, single-line summary of the first task. The `share` workflow (`teamai skill get share`) summarizes the session and pushes a learning document directly to the team repo. Each session is prompted at most once. Teams can switch the hint off with `sharing.contributeHint.enabled: false` in `teamai.yaml` (members: `contributeHintEnabled` in local config) while keeping the rest of the Stop hook. The hint also needs recall to be on (it is off by default), because the workflow it points at is served only then. ### Team Knowledge Recall diff --git a/docs/product-overview.zh-CN.md b/docs/product-overview.zh-CN.md index f3396d89..5a7faf0b 100644 --- a/docs/product-overview.zh-CN.md +++ b/docs/product-overview.zh-CN.md @@ -115,10 +115,10 @@ Session 结束时,Stop hook 按**摩擦信号**对 session 评分——这些 Task: Fix duplicate project-level Hook injection -Consider running /teamai-share-learnings to summarize what you learned and share it with your team. +Consider running `/teamai share what this session taught me` to summarize what you learned and share it with your team (or run `teamai skill get share`). ``` -提示会列出实际触发它的非零摩擦信号;如果能取得首个任务摘要,还会在脱敏、单行化后附上任务上下文。`/teamai-share-learnings` skill 自动总结 session 经验并推送到团队仓库。每个 session 最多提示一次。团队可在 `teamai.yaml` 设置 `sharing.contributeHint.enabled: false` 关闭该提示(成员可用本地配置 `contributeHintEnabled` 覆盖),Stop hook 的其余功能不受影响。 +提示会列出实际触发它的非零摩擦信号;如果能取得首个任务摘要,还会在脱敏、单行化后附上任务上下文。`share` 工作流(`teamai skill get share`)自动总结 session 经验并推送到团队仓库。每个 session 最多提示一次。团队可在 `teamai.yaml` 设置 `sharing.contributeHint.enabled: false` 关闭该提示(成员可用本地配置 `contributeHintEnabled` 覆盖),Stop hook 的其余功能不受影响。该提示还需要开启 recall(默认关闭),因为它指向的工作流只在 recall 开启时提供。 ### 团队知识检索 diff --git a/docs/usage-guide.md b/docs/usage-guide.md index 35f53d26..396d7cae 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -455,9 +455,34 @@ teamai list --source local # Skills under each installed agent teamai list --agent claude --verbose teamai list env --reveal # Show env values in plaintext (default: masked) -teamai skill # Equivalent to teamai list skills --source all +teamai skill # teamai list skills --source all, then the CLI-served built-in catalog teamai skill show hai-deploy-test # View a single skill's source / contributor / install locations / description summary -``` + +teamai skill list --json # The built-in skills the installed CLI serves, machine-readable +teamai skill get core # Print a built-in workflow: core | setup | wiki | share +teamai skill get wiki --full # ...with its references and templates appended +teamai skill path wiki # The packaged directory, for the scripts a skill ships +``` + +#### Built-in skills are versioned with the CLI + +The built-in workflows (`core`, `setup`, `wiki`, `share`) ship inside the npm package +and are printed by the installed binary with `teamai skill get`, so what an agent reads +always matches the CLI version it is running — `npm i -g teamai-cli@latest` is the +update, with no pull needed for the content to be current. Agents receive a single file +from the CLI, `~/./skills/teamai/SKILL.md` (or wherever that tool keeps team skills: OpenClaw's +workspace, `HERMES_HOME`), a small discovery stub that points at +those commands. Older releases copied the whole tree into every agent directory, where it +went stale between pulls; `teamai pull` removes those leftovers, keeping a copy of every +removed file under `~/.teamai/removed-skills/`, one directory per pull (until `teamai uninstall`, +which removes `~/.teamai/` and this archive with it). Only files whose content a release shipped +are removed: a packaged file you edited, or a skill of your own under one of the old names, is +yours and stays. A directory that also holds a file of your own is kept, with only the +packaged files removed, and named in the pull output. `share` is served only while recall is +on (off by default; `sharing.recall.enabled: true` in `teamai.yaml` for the team, or +`teamai recall enable` for one machine): until then `teamai skill get share` refuses and says so. +It also refuses on a read-only HTTP source, where `teamai contribute` cannot write. The legacy names still +resolve: `teamai skill get team-wiki-codebase` serves `wiki`. --- @@ -896,10 +921,10 @@ The AI tracks your coding sessions via Hooks. When a session ends (the Stop hook Task: Fix duplicate project-level Hook injection -Consider running /teamai-share-learnings to summarize what you learned and share it with your team. +Consider running `/teamai share what this session taught me` to summarize what you learned and share it with your team (or run `teamai skill get share`). ``` -The reminder lists the non-zero friction signals that triggered it. When the first task is available, it also includes a redacted, single-line task summary so you can decide whether the session is worth sharing. Using the built-in `/teamai-share-learnings` skill, the AI will automatically summarize the session's learnings and contribute them to the team knowledge base. Each session is prompted at most once. +The reminder lists the non-zero friction signals that triggered it. When the first task is available, it also includes a redacted, single-line task summary so you can decide whether the session is worth sharing. Using the built-in `share` workflow (`teamai skill get share`), the AI will automatically summarize the session's learnings and contribute them to the team knowledge base. Each session is prompted at most once. For the Codex family (`codex`, `codex-internal`, `tcodex`), the Stop hook saves contribution and knowledge-reference reminders for the next UserPromptSubmit in the same session. It does not force an extra agent turn. Contribution reminders are delivered once and discarded if you contribute before the next prompt. @@ -920,7 +945,9 @@ Teams that route knowledge sharing through their own review flow (for example, a | User override | `~/.teamai/config.yaml` | `contributeHintEnabled` | `true` / `false`, takes priority over the team default | | Environment variable | shell | `TEAMAI_CONTRIBUTE_HINT_DISABLED=1` | Force-disables the hint (emergency kill switch) | -Only the nudge is affected: friction scoring, `teamai contribute --file`, and `/teamai-share-learnings` keep working when invoked manually. +Only the nudge is affected: friction scoring, `teamai contribute --file`, and `/teamai` keep working when invoked manually. + +The reminder is also withheld while recall is off (the default until `sharing.recall.enabled: true` in `teamai.yaml`, or `teamai recall enable` on one machine): it points at the `share` workflow, and `teamai skill get share` refuses until recall is on. It never appears on a read-only HTTP source, where `share` refuses too. ### Searching knowledge @@ -1841,7 +1868,7 @@ sharing: coAuthor: enabled: false # optional; strip AI-tool commit trailers team-wide contributeHint: - enabled: true # optional; false = no /teamai-share-learnings nudge after high-friction sessions + enabled: true # optional; false = no /teamai nudge after high-friction sessions intervention: correctionKeywords: [] # optional; extra course-correction words merged with the built-in zh/en/ja list webhooks: # optional; notify external endpoints on team events (see "Webhook notifications") diff --git a/docs/usage-guide.zh-CN.md b/docs/usage-guide.zh-CN.md index d0fa0ee4..5110a624 100644 --- a/docs/usage-guide.zh-CN.md +++ b/docs/usage-guide.zh-CN.md @@ -432,10 +432,30 @@ teamai list --source local # 各已安装 agent 下的 skills teamai list --agent claude --verbose teamai list env --reveal # 明文显示 env(默认脱敏) -teamai skill # 等价于 teamai list skills --source all +teamai skill # 先输出 teamai list skills --source all,再列出 CLI 内置 skill 目录 teamai skill show hai-deploy-test # 看单个 skill 的来源 / 贡献者 / 安装位置 / 描述摘要 + +teamai skill list --json # 当前 CLI 提供的内置 skill 清单(机器可读) +teamai skill get core # 打印内置工作流:core | setup | wiki | share +teamai skill get wiki --full # 同时附上该 skill 的 references 与 templates +teamai skill path wiki # 打印打包目录,用于运行 skill 自带的脚本 ``` +#### 内置 skill 随 CLI 一起版本化 + +内置工作流(`core`、`setup`、`wiki`、`share`)随 npm 包一起发布,由已安装的 CLI 通过 `teamai skill get` +按需打印,因此 agent 读到的内容始终与正在运行的 CLI 版本一致——`npm i -g teamai-cli@latest` 本身就是更新, +无需 `teamai pull` 内容就是最新的。每个 agent 只收到一个文件:`~/./skills/teamai/SKILL.md`(或该工具存放团队 skill 的位置:OpenClaw 的 workspace、`HERMES_HOME`), +一个指向这些命令的小型发现入口(stub)。旧版本会把整棵目录复制到每个 agent 下,两次 pull 之间内容会过时; +`teamai pull` 会清除这些残留,并把每个被删除的文件先复制到 `~/.teamai/removed-skills/` 下(每次 pull 一个目录; +`teamai uninstall` 会删除 `~/.teamai/`,这份备份也随之删除)。只删除内容与某个发布版本完全一致的文件:你改过的打包文件, +或你自己用旧名字写的 skill,都属于你,会保留。目录里若还有你自己的文件, +只删除其中的打包文件,保留该目录和你的文件,并在 pull 输出中点名。`share` 只在开启 recall 后才会提供(默认关闭; +团队在 `teamai.yaml` 设置 `sharing.recall.enabled: true`,或单台机器运行 `teamai recall enable`):在此之前, +`teamai skill get share` 会拒绝并说明原因。 +只读 HTTP 源上它同样会拒绝,因为 `teamai contribute` 无法写入。旧名字仍然可用: +`teamai skill get team-wiki-codebase` 等价于 `wiki`。 + --- ## 日常使用 @@ -868,10 +888,10 @@ AI 通过 Hooks 追踪你的编码会话。当会话结束时(Stop hook), Task: Fix duplicate project-level Hook injection -Consider running /teamai-share-learnings to summarize what you learned and share it with your team. +Consider running `/teamai share what this session taught me` to summarize what you learned and share it with your team (or run `teamai skill get share`). ``` -提醒会列出实际触发它的非零摩擦信号;如果能取得首个任务,还会附上脱敏、单行化后的任务摘要,便于判断本次 session 是否值得分享。使用内置 skill `/teamai-share-learnings`,AI 会自动总结本次 session 经验并贡献到团队知识库。每个 session 最多提示一次。 +提醒会列出实际触发它的非零摩擦信号;如果能取得首个任务,还会附上脱敏、单行化后的任务摘要,便于判断本次 session 是否值得分享。使用内置 `share` 工作流(`teamai skill get share`),AI 会自动总结本次 session 经验并贡献到团队知识库。每个 session 最多提示一次。 在 Codex 系列(`codex`、`codex-internal`、`tcodex`)中,Stop hook 会暂存贡献和知识引用提醒,在同一会话的下一次 UserPromptSubmit 交付,不会强制开启额外一轮。贡献提醒只交付一次;若下一次输入前已经贡献,则丢弃该提醒。 @@ -892,7 +912,9 @@ teamai contribute --file /tmp/session.md --scope project | 用户覆盖 | `~/.teamai/config.yaml` | `contributeHintEnabled` | `true` / `false`,优先级高于团队默认 | | 环境变量 | shell | `TEAMAI_CONTRIBUTE_HINT_DISABLED=1` | 强制关闭提醒(紧急开关) | -只影响提醒本身:摩擦评分、`teamai contribute --file` 和手动调用 `/teamai-share-learnings` 不受影响。 +只影响提醒本身:摩擦评分、`teamai contribute --file` 和手动调用 `/teamai` 不受影响。 + +未开启 recall 时(默认关闭;团队在 `teamai.yaml` 设置 `sharing.recall.enabled: true`,或单台机器运行 `teamai recall enable`)也不会显示这条提醒:提醒指向 `share` 工作流,而 recall 关闭时 `teamai skill get share` 会拒绝执行。只读 HTTP 源上这条提醒也从不出现,因为 `share` 同样会拒绝。 ### 搜索知识 @@ -1788,7 +1810,7 @@ sharing: coAuthor: enabled: false # 可选,为全团队去除 AI 工具提交尾注 contributeHint: - enabled: true # 可选,false = 高摩擦 session 结束后不再提示 /teamai-share-learnings + enabled: true # 可选,false = 高摩擦 session 结束后不再提示 /teamai intervention: correctionKeywords: [] # 可选,额外的纠偏词,与内置中/英/日列表合并 webhooks: # 可选,在团队事件发生时通知外部端点(见"Webhook 通知") diff --git a/package.json b/package.json index 739df072..c05d9168 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "files": [ "dist/**/*.js", "skills", + "skill-data", + "!**/__pycache__", + "!**/*.pyc", "agents", "README.md", "CHANGELOG.md", diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md new file mode 100644 index 00000000..b9a02ca6 --- /dev/null +++ b/skill-data/core/SKILL.md @@ -0,0 +1,114 @@ +--- +name: core +description: >- + TeamAI daily workflow: route a /teamai request, sync with pull and push, inspect status, + diagnose with doctor, and reach the specialized workflows. Loaded by the teamai discovery stub. +--- + +# teamai — daily workflow + +You are guiding a user through TeamAI. **They may not know Git.** You run the +commands; they only make choices when you ask. Follow the steps literally — +do not skip, reorder, or invent commands. + +## Start here + +Look at what the user typed after `/teamai`. + +**If they gave NO scenario** (bare `/teamai`, or only greetings/no task): +print the menu below **exactly**, then **STOP and wait**. Take no other action — +do not run any command, do not load another skill yet. + +``` +teamai — Team AI Skills & Rules Sync + +Usage examples (copy one to get started): + + 🏗️ Admin — set up a new team repo: + /teamai Help me set up TeamAI for my team from scratch + + 🤝 Member — join an existing team: + /teamai Help me join my team's TeamAI, repo URL is https://... + + 🔧 Admin — daily management (publish & update skills, rules, MCP, env): + /teamai I already have TeamAI set up, help me manage it + + 📊 Anyone — open the team dashboard: + /teamai Open the TeamAI dashboard + + 💡 Member — share a skill with the team (just ask in plain language): + /teamai Share this skill with my team + + 🗑️ Anyone — remove TeamAI from this machine: + /teamai Uninstall TeamAI +``` + +**If they DID describe a scenario**, match it to one row and follow what it loads. + +| The user wants to… | Load this | +|-----------------------------------------------------------------------|------------------------------------------------| +| Set up a team from scratch, join a team, manage one, or uninstall | `teamai skill get setup` | +| Publish a skill, rule or doc they already have | `{SKILL_DIR}/references/contribute-member.md` | +| Share what this session taught them | `teamai skill get share` | +| Understand a large multi-repo codebase, build an architecture wiki | `teamai skill get wiki` | +| Sync now, see differences, diagnose | `teamai pull` · `teamai status` · `teamai doctor` | +| Open the team dashboard | `teamai dashboard` — it starts a local server (default port 3721); give the user the URL | +| Something broke | `{SKILL_DIR}/references/troubleshooting.md` | + +If the request is ambiguous (e.g. "help me with teamai" with no direction), +ask ONE short question to pick a row, then proceed. + +Sharing a session's learnings needs no menu choice: TeamAI prompts on its own at +the end of a session that produced something worth sharing, and that prompt means +`teamai skill get share`. (Only when recall is on; it is off by default. The team turns it on with +`sharing.recall.enabled: true` in `teamai.yaml`, a member with `teamai recall enable`; +while it is off, `teamai skill get share` says so.) + +## Global rules + +1. **Reply in the user's language — including every example and hand-off blurb.** + Answer in whatever language the user used, for the whole conversation. This + applies to **everything you write**: the invite line you give an admin to + forward, the one-line explanations, the "what's next" summary — all of it is + translated before you show it. *Only* commands, flags, URLs, file paths and + code identifiers stay verbatim (never translate `teamai pull`, `--scope user`, + `/teamai`, a repo URL). +2. **Never teach Git.** Do not mention branches, commits, clone, or push/pull of + Git itself. TeamAI hides all of that. The user thinks in terms of "my team's + skills", not repositories. +3. **You run the commands.** Only pause to ask the user when you need a web login, + a value only they know, or a genuine either/or choice. Show each command before + you run it, in one short line. +4. **Detect the current AI tool first.** TeamAI behaves differently per host. Note + which tool this conversation is running in (Claude Code, Cursor, CodeBuddy, + WorkBuddy, ChatGPT App, Codex, OpenCode, Kiro, Gemini CLI, …). When you reopen a + session, use the name of **this** tool — do not assume Claude Code or Cursor. + Some hosts need extra manual steps for hooks — see the troubleshooting + reference ("Agent-specific caveats"). + +## Daily commands + +```bash +teamai pull # Sync team resources into local AI tools now +teamai push # Publish your local skills/rules/docs to the team +teamai status # Show local vs team differences +teamai doctor # Diagnose configuration and hook problems +teamai list # List resources (skills|rules|docs|env|agents|hooks|mcp) +teamai recall # Search what the team has already learned +``` + +Every other command, every flag, and the flags `--help` hides live in the +generated reference below. Read it instead of guessing a flag. + +## References + +In the files below, `{SKILL_DIR}` is the directory `teamai skill path core` prints; a reference file you open on its own writes that directory as `SKILL_DIR` in braces. + +| File | When to load it | +|---|---| +| `{SKILL_DIR}/references/commands.md` | Before using any command not in the daily list, or any flag. Generated from the CLI's own command table, so it cannot drift. | +| `{SKILL_DIR}/references/troubleshooting.md` | A command fails, a hook does not fire, or a host needs manual steps. | +| `{SKILL_DIR}/references/contribute-member.md` | A member wants to publish a skill, rule or doc they already have. Any member can, not just admins. | + +`teamai skill get core --full` prints this skill with all three references +appended. Load a single file above when you only need one. diff --git a/skill-data/core/references/commands.md b/skill-data/core/references/commands.md new file mode 100644 index 00000000..8c0493af --- /dev/null +++ b/skill-data/core/references/commands.md @@ -0,0 +1,339 @@ +# teamai command reference + +Every public command the installed CLI accepts, rendered from its own command +table. Hidden commands are left out: they are hook plumbing the CLI runs itself, +never something to type. Flags marked `(hidden)` work but are absent from +`--help`, so treat this file — not `--help` — as the complete list of flags. + +Generated: do not edit by hand. Regenerate with +`npx vitest run commands-reference -u` after changing a command or a flag. + + +## Global options + +- `-V, --version` — output the version number +- `--dry-run` — Preview mode, no changes made +- `-v, --verbose` — Verbose output + +## init + +- `teamai init [repo]` — Initialize teamai (configure Git provider, clone repo, register member) + - `--repo ` — Team repo (alias of the positional argument) + - `--http ` — Git-free HTTP team repo (read-only consumer; only needs an API key) + - `--self` — Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch. + - `--token ` — API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN. + - `--scope ` — Install scope: project (default, /.teamai + /.claude) or user (~/.teamai + ~/.claude) + - `--inherit-user-scope` — In project scope, also sync safe user-scope resources and search its knowledge + - `--no-inherit-user-scope` — Disable user-scope inheritance for this project + - `--role ` — Primary role ID (e.g. hai_dev) for non-interactive setup + - `--project ` — Active logical project(s) from manifest/projects.yaml (comma-separated); scopes which project resources and learnings this directory syncs. Pass "all" to activate every project the manifest declares (a snapshot taken now) + - `--agent ` — AI tools to set up (e.g. claude, codex, cursor, codebuddy, workbuddy, dsh). Repeatable or comma-separated. In single-repo mode, selects which tool dirs to create; omit for an interactive picker. Additive on repeated runs. + - `--force` — Overwrite existing config without confirmation + +## push + +- `teamai push` — Push local resources to team repo + - `--all` — Push all without confirmation + - `--skill ` — Push a specific skill by path (e.g., ~/.claude/skills/hai/my-skill or skills/hai_dev/my-skill) + - `--role ` — Namespace for new skills, rules and agents (skills//, rules//, agents//) + - `--project ` — Target a project: each new resource goes to that project's namespace for its own type — skills, knowledge for rules, agents (from manifest/projects.yaml) + +## pull + +- `teamai pull` — Pull team resources and inject into local AI tools + - `--silent` — Silent mode (for hooks) + - `--force` — Force full sync even if repo is unchanged + +## status + +- `teamai status` — Show local vs team repo diff + - `--all` — List every project data partition under ~/.teamai/projects (flags stale/orphan ones) + +## list + +- `teamai list [type]` — List resources (skills|rules|docs|env|agents|hooks|mcp). For skills, --source local/all also scans installed AI agent skill directories. + - `--source ` — Where to look for skills: repo | local | all + - `--agent ` — Filter local agents by id (only applies to skills) + - `--reveal` — Show env values in plaintext (default: masked) + +## skill + +- `teamai skill` — List and inspect skills (default: repo + installed agents, then the CLI-served catalog) + - `teamai skill list` — List team and installed skills, then the built-in catalog the CLI serves + - `--json` — Output the CLI-served built-in skill catalog as JSON + - `teamai skill get [names...]` — Print built-in skill content served by the installed CLI + - `--full` — Append the skill's references/ and templates/ files + - `--all` — Print every skill the CLI serves + - `teamai skill path ` — Print the packaged directory of a built-in skill (for scripts and templates) + - `teamai skill show ` — Show skill metadata: source / contributors / installed agents / description + - `teamai skill exclude` — Manage per-user skill exclusion (skip sync without affecting team repo) + - `teamai skill exclude list` — List excluded skills + - `teamai skill exclude add ` — Add skill(s) to the exclude list + - `teamai skill exclude remove ` — Remove skill(s) from the exclude list + +## members + +- `teamai members` — Manage team members + - `teamai members list` — List team members + +## remove + +- `teamai remove ` — Remove resource(s) from team repo and all local AI tools (type: skills|rules|agents|mcp) + - `--force` — Skip confirmation prompt + +## packages + +- `teamai packages [target]` — Install team npm packages and Claude plugins declared in teamai.yaml + - `-g, --global` — Install an npm target globally (for CLI tools) + - `--registry ` — Use a specific npm registry for this target + - `--npm` — Treat an ambiguous target as an npm package + - `--claude` — Treat the target as a Claude plugin + - `teamai packages install [target]` — Install team npm packages and Claude plugins declared in teamai.yaml + - `-g, --global` — Install an npm target globally (for CLI tools) + - `--registry ` — Use a specific npm registry for this target + - `--npm` — Treat an ambiguous target as an npm package + - `--claude` — Treat the target as a Claude plugin + +## doctor + +- `teamai doctor` — Diagnose configuration issues + - `--json` — Output the report as JSON (suitable for CI) + +## roles + +- `teamai roles` — Manage team roles and resource namespaces + - `teamai roles init` — Create a roles manifest for the team repo (admin) + - `teamai roles list` — List all defined roles and your current role + - `teamai roles set ` — Set your primary role (updates local config) + - `--add ` — Additional roles to include + - `teamai roles add ` — Add a new role to the manifest (admin) + - `--namespaces ` — Comma-separated resource namespaces (e.g. common,hai) + - `-d, --description ` — Description for the role + - `teamai roles remove ` — Remove a role from the manifest (admin) + - `teamai roles update ` — Update a role in the manifest (admin) + - `--add-namespaces ` — Comma-separated namespaces to add + - `--remove-namespaces ` — Comma-separated namespaces to remove + - `-d, --description ` — New description for the role + +## projects + +- `teamai projects` — Manage multi-project resource distribution (orthogonal to roles) + - `teamai projects list` — List defined projects and the ones active in this directory + - `teamai projects set [ids...]` — Set the projects active in this directory (comma-separated or repeated; empty to clear) + - `teamai projects members ` — List members registered for a project + +## tags + +- `teamai tags` — Manage tag-based skill/rule filtering + - `teamai tags list` — List all available tags and subscription status + - `teamai tags subscribe ` — Subscribe to tags (only matching skills/rules will be synced) + - `teamai tags unsubscribe ` — Unsubscribe from tags + - `teamai tags add ` — Add tags to a skill or rule in tags.yaml (admin) + + Resource type: "skills" or "rules" + Name of the skill or rule (directory name) + One or more tags to add + + Examples: + $ teamai tags add skills hai-deploy hai infra + $ teamai tags add rules common-coding-style coding best-practices + + - `teamai tags remove ` — Remove tags from a skill or rule in tags.yaml (admin) + + Resource type: "skills" or "rules" + Name of the skill or rule (directory name) + One or more tags to remove + + Examples: + $ teamai tags remove skills hai-deploy infra + $ teamai tags remove rules common-coding-style best-practices + + +## source + +- `teamai source` — Manage cross-team skill sources + - `teamai source add ` — Add a cross-team source repo + - `--name ` — Alias for this source + - `teamai source remove ` — Remove a source and clean up its skills + - `teamai source add-http ` — Add an HTTP source (report/sync/ack) alongside a git main repo + - `--token ` — API token for the HTTP endpoint (stored 0600, never committed) + - `--force` — Overwrite an existing HTTP source config + - `teamai source remove-http` — Remove the HTTP source and clean up its resources + - `teamai source list` — List all configured sources + - `teamai source browse ` — Browse public skills from a source + +## update + +- `teamai update` — Check for updates and upgrade teamai CLI + - `--check` — Only check if an update is available, do not install + +## uninstall + +- `teamai uninstall` — Remove all teamai-managed resources and hooks from this machine + - `--force` — Skip confirmation prompt + - `--agent ` — Only uninstall this agent's resources; shared resources go only if it is the last tool + +## env + +- `teamai env` — Manage team environment variables + - `--reveal` — Show env variable values in plaintext (default: masked) + - `teamai env list` — List team environment variables + - `--reveal` — Show env variable values in plaintext (default: masked) + - `teamai env add ` — Add or update a team environment variable + - `-d, --description ` — Description for the variable + - `teamai env remove ` — Remove a team environment variable + +## hooks + +- `teamai hooks` — Manage teamai hooks in AI tool settings + - `teamai hooks list` — List hook install status + effective built-in (A) and team (B) hooks + - `teamai hooks inject` — Inject teamai hooks into all AI tool settings + - `--silent` — Silent mode (suppress success message) + - `teamai hooks remove` — Remove teamai hooks from all AI tool settings + +## mcp + +- `teamai mcp` — Manage team MCP servers across AI tools + - `teamai mcp list` — List team MCP servers and their per-tool install status + - `teamai mcp inject` — Inject team MCP servers into all AI tool configs + - `--dry-run` — Show what would change without writing + - `--force` — Overwrite servers that collide with user-owned entries + - `teamai mcp remove` — Remove all teamai-managed MCP servers from AI tool configs + +## webhook + +- `teamai webhook` — Manage webhook integrations for team notifications + - `teamai webhook list` — List configured webhook endpoints + - `teamai webhook test` — Send test event to webhook endpoints + - `--url ` — Test specific endpoint URL + +## stats + +- `teamai stats` — Show local skill usage statistics + - `--by-repo` — Break usage down per repository + - `--by-time` — Show activity by hour of day + +## session + +- `teamai session` — Record and inspect coding-session summaries + - `teamai session save` — Record a privacy-scrubbed summary of a coding session to a local monthly log + - `--session-id ` — Session to record (default: most recent, or $CLAUDE_SESSION_ID) + - `--push` — Also push the summary to the team repo (feeds `teamai digest`) + - `--force` — Push even if the session is not flagged as valuable + - `--include-prompt` — Include the redacted first-prompt line in the pushed summary (default: off) + - `--scope ` — Config scope for --push: user | project (default: auto-detect) + +## digest + +- `teamai digest` — Generate weekly team activity digest + +## dashboard + +- `teamai dashboard` — Start the AI coding session dashboard (Web UI) + - `-p, --port ` — Port number + +## bind-project + +- `teamai bind-project` — Bind the current workspace to a ClawPro project for HTTP local-agent sync + - `--project-id ` — Project ID from /projects/mine + - `--skip` — Mark current workspace as skipped (never prompt again) + +## contribute + +- `teamai contribute` — Contribute session knowledge to team repo + - `--file ` — Path to the contribution document + - `--title ` — Title for the contribution document + - `--session-id <id>` — Session ID for dedup tracking + - `--scope <scope>` — Target scope: user or project + +## recall + +- `teamai recall [query...]` — Search team learnings knowledge base + - `--depth <level>` — Recall depth: route (entry-points only) | context (module-level, default) | lookup (full graph traversal) + - `--check` — Relevance precheck only: print RELEVANT/NOT_RELEVANT + top score; no file reads, no upvote + - `teamai recall disable` — Disable automatic knowledge-base recall + - `teamai recall enable` — Enable automatic knowledge-base recall + - `teamai recall status` — Show recall feature status + - `teamai recall feedback` — Record manual feedback for a recalled document + - `--positive <docId>` — Upvote a document (marks as actually useful) + - `--negative <docId>` — Record negative signal for a document + - `teamai recall maintenance` — Automatic maintenance of team knowledge base + - `--prune` — Remove low-confidence learnings + - `--threshold <n>` — Confidence threshold for pruning (default 0.15) + - `--archive` — Move to archive/ instead of deleting + - `--confidence-writeback` — Update frontmatter confidence scores + - `--update-quality` — Find stale docs/rules/skills and suggest updates + - `--dry-run` — Show what would be done without making changes + - `teamai recall promote [learningId]` — Promote a high-confidence learning to formal knowledge (docs/skills/rules) + - `--category <cat>` — Target category: skills | rules | docs + - `--dry-run` — Show what would be done without making changes + +## import + +- `teamai import` — Import knowledge from local directories, remote repos, organizations, MRs, or iWiki + - `--dir <path>` — Extract code knowledge from a local directory (same as --from-repo but no clone) + - `--from-claude` (hidden) — Scan Claude/Cursor rule directories (~/.claude/rules, ~/.cursor/rules) + - `--from-mr <url>` — Extract learning from merged MR/PR and trigger incremental teamwiki update + - `--from-iwiki <space-id-or-url>` — Import documents from iWiki Space ID or page URL (requires TAI_PAT_TOKEN) + - `--resume` (hidden) — Resume an interrupted import session + - `--all` — Accept all suggestions without interactive confirmation + - `--output <path>` (hidden) — Write drafts to this directory instead of pushing to team repo + - `--from-repo <url>` — Clone a remote repo and generate per-repo codebase summary + - `--ssh` (hidden) — Force SSH clone even if HTTPS token is available + - `--domain <name>` (hidden) — Skip AI recommendation and assign repo to this domain explicitly + - `--from-repo-list <path>` — Batch import repos from a YAML whitelist + - `--concurrency <n>` (hidden) — Concurrent repos for --from-repo-list (default 3) + - `--incremental` — Use cached clone with fetch+reset (with --from-repo or --from-repo-list) + - `--skip-enrich` — Skip AI enrichment (only clone + extract + graph, no LLM calls) + - `--from-org <org>` — List repos under an org and generate a repo whitelist + - `--max-repos <n>` (hidden) — Cap on repos pulled from --from-org (default 200) + - `--exclude-archived` (hidden) — Exclude archived repos from --from-org (default true) + - `--include-pattern <re>` (hidden) — Regex to include repos by full name (used with --from-org) + - `--exclude-pattern <re>` (hidden) — Regex to exclude repos by full name (used with --from-org) + - `--skip-import` (hidden) — Only write drafts; skip the actual --from-repo-list run + - `--iwiki-dual` (hidden) — Enable dual-output mode for --from-iwiki (write codebase sections in addition to learning) + - `--require-review` (hidden) — Defer codebase section writes to .teamai/pending-review.jsonl for human review + - `--cache-status` — Show import cache status (repos cached, disk usage) + - `--cache-gc` — Garbage-collect stale import cache entries + - `--json` — Output cache status or GC result as JSON + - `--max-bytes <n>` (hidden) — Override capacity cap for --cache-gc + - `--stale-days <n>` (hidden) — Threshold for stale-eviction in days (default 30) + +## codebase + +- `teamai codebase` — Inspect and maintain team-codebase outputs + - `--extract [path]` — Extract code knowledge and build graph from source + - `--incremental` (hidden) — Only re-extract changed files (requires prior manifest) + - `--project <name>` (hidden) — Project slug for --extract (defaults to directory name) and required for --deep-enrich + - `--max-files <n>` (hidden) — Max source files to scan (default: 200) + - `--upgrade-wiki` (hidden) — Migrate docs/team-codebase/ to teamwiki/ graph format + - `--lint` — Run global consistency lint over the teamwiki knowledge graph + - `--reconcile` — Reconcile product and code knowledge in teamwiki + - `--deep-enrich` — Generate deep knowledge docs from extracted evidence + - `--fix` (hidden) — Deprecated: teamwiki lint has no autofix; runs lint in report-only mode + - `--status` — Show knowledge-base git baseline (headSha / repoUrl / branch) + - `--severity <level>` (hidden) — Minimum severity to report: high|medium|low|info + - `--json` — Output report as JSON (suitable for CI) + - `--output <path>` (hidden) — Custom teamwiki output root directory + +## review + +- `teamai review [id]` — Inspect and process .teamai/pending-review.jsonl items + - `--apply` — Apply the change for the given id (only for codebase-section) + - `--reject` — Reject the given id without applying + - `--reason <msg>` — Reason for reject + - `--all-apply` — Apply all items at or below --max-risk + - `--max-risk <level>` — Risk ceiling for --all-apply: high|medium|low (default medium) + - `--json` — Machine-readable output + +## ci + +- `teamai ci` — CI pipeline integration commands + - `teamai ci extract-mr` — Extract knowledge from MR/PR and post as comment or write to team repo + - `--url <url>` — MR/PR web URL + - `--mode <mode>` — Operation mode: comment | write | both + - `--team-repo <path>` — Team knowledge repo path (required for write mode) + - `--comment-marker <marker>` — HTML comment anchor for idempotent updates + - `--write-mode <mode>` — Write strategy: direct | pending-review + - `--output <dir>` — Write artifacts to directory + - `--individual-comments` — Post each suggestion as separate comment with reaction/resolve support diff --git a/skills/teamai/references/contribute-member.md b/skill-data/core/references/contribute-member.md similarity index 81% rename from skills/teamai/references/contribute-member.md rename to skill-data/core/references/contribute-member.md index ccf8bd9b..73590fde 100644 --- a/skills/teamai/references/contribute-member.md +++ b/skill-data/core/references/contribute-member.md @@ -3,23 +3,22 @@ Goal: the user turns something they built into team knowledge everyone can pull. **Any member can do this — you do not need to be an admin.** The usual entry point is the user just asking in plain language, e.g. *"share this xxx skill with my -team"* / *"把这个 xxx skill 分享给团队"* — then you run the publish for them. +team"*, in whatever language they work in — then you run the publish for them. ## Which kind of contribution? - **A learning** (a lesson, a gotcha, how you solved something) → this is - **automatic**: TeamAI prompts at the end of a session worth sharing and the - dedicated **`teamai-share-learnings`** skill takes over (it summarizes the + **automatic** once recall is on (off by default): TeamAI prompts at the end of a session worth sharing and the + dedicated `share` workflow (`teamai skill get share`) takes over (it summarizes the session and runs `teamai contribute`). The user does not come through this flow - for it. (Step A below is only a manual fallback for when that skill isn't - available.) + for it. (Step A below is only a manual fallback for while recall is off.) - **A reusable skill** (a `SKILL.md` others invoke) → author the skill, then `teamai push` (Step B — the main purpose of this reference). ## Step A — Contribute a learning by hand (fallback only) -> Prefer the **`teamai-share-learnings`** skill. Use these manual steps only if it -> is unavailable in the current tool. +> Prefer the `share` workflow (`teamai skill get share`). Use these manual steps only while +> it refuses because recall is off. 1. Write a short Markdown doc that captures the lesson. Keep it concrete and actionable — a knowledge base, not a diary. Include YAML frontmatter for search @@ -53,9 +52,8 @@ team"* / *"把这个 xxx skill 分享给团队"* — then you run the publish fo The doc lands in the team's `learnings/` and appears for teammates on their next `teamai pull`. It is also searchable via `teamai recall`. -> Tip: if there is a dedicated learnings skill available in this tool -> (`teamai-share-learnings`), you can use it to auto-summarize the current session -> instead of writing the doc by hand. +> Tip: while recall is on, `teamai skill get share` auto-summarizes the current +> session instead of you writing the doc by hand. ## Step B — Contribute a reusable skill diff --git a/skills/teamai/references/troubleshooting.md b/skill-data/core/references/troubleshooting.md similarity index 99% rename from skills/teamai/references/troubleshooting.md rename to skill-data/core/references/troubleshooting.md index d4f87ac5..af2ec6ad 100644 --- a/skills/teamai/references/troubleshooting.md +++ b/skill-data/core/references/troubleshooting.md @@ -115,7 +115,7 @@ session and verify with `teamai pull` + `teamai list`. The sandbox **does not add hooks automatically** after `teamai init`. The user must **manually edit the config file to register the hook** so auto-sync works. Walk them through opening the tool's config and adding the TeamAI session-start -hook entry; if unsure of the exact config, run `teamai doctor` and `teamai hooks` +hook entry; if unsure of the exact config, run `teamai doctor` and `teamai hooks list` to see what should be present, then have them replicate it. Until then, they can sync with a manual `teamai pull`. diff --git a/skill-data/setup/SKILL.md b/skill-data/setup/SKILL.md new file mode 100644 index 00000000..68313331 --- /dev/null +++ b/skill-data/setup/SKILL.md @@ -0,0 +1,76 @@ +--- +name: setup +description: >- + TeamAI day 0 and repo lifecycle: create a team repo as admin, join an existing team as a member, + manage members, roles, MCP and env, and uninstall. Loaded on demand by the teamai discovery stub. +--- + +# teamai — setup and lifecycle + +You run the commands; the user only makes choices when you ask. **They may not +know Git** — never explain branches, commits or clones. + +## Before anything + +```bash +node --version # must be >= 20 +teamai --version # install once with: npm install -g teamai-cli +``` + +Then pick the flow. A user **setting up a new team** becomes its admin and creates +the repo; a user **joining an existing team** needs a repo URL from their admin. A +would-be member without a URL is still the **join** flow — `join-member.md` tells +them how to ask for it. Do not send them to the create-repo flow because the URL +is missing. + +| The user wants to… | Load this | +|-----------------------------------------------------------------------------|---------------------------------------------| +| Set up TeamAI for a team from scratch (create the repo) | `{SKILL_DIR}/references/setup-admin.md` | +| Join their team, with or without a repo URL | `{SKILL_DIR}/references/join-member.md` | +| Publish or update skills, rules, MCP, env; invite members; manage roles | `{SKILL_DIR}/references/manage-admin.md` | +| Remove TeamAI from this machine | `{SKILL_DIR}/references/uninstall.md` | +| Publish one skill or contribute a doc | `"$(teamai skill path core)/references/contribute-member.md"` | +| Anything that breaks along the way | `"$(teamai skill path core)/references/troubleshooting.md"` | + +Supported Git providers are Tencent TGit, GitHub, GitLab and CNB; +`{SKILL_DIR}/references/setup-admin.md` carries the detection probe, the sign-in +and create-repo URLs, and the per-provider caveats, and points at +`{SKILL_DIR}/references/provider-tgit.md` for everything TGit-specific. + +## Rules for these flows + +1. **Always use a full URL** for the team repo (e.g. + `https://github.com/yourorg/yourrepo`). Never the `owner/repo` short form. +2. **Don't limit which AI tools get set up — cover all of them by default.** + Unless the user names specific tools, do **not** pass `--agent` to restrict the + install. Let `teamai init` set up every AI tool already installed (omitting + `--agent` gives an interactive picker; select all detected tools). **After init, + report which agents were set up** — in the user's language, which tools now + auto-start TeamAI, and which detected tools were skipped and why (e.g. Codex + trust-gate, CodeBuddy design). Verify the real per-tool result with + `teamai doctor` and `teamai hooks list`. +3. **After init, resources appear on the NEXT session.** `teamai init` injects a + session-start hook that auto-runs `teamai pull`. Empty skills/rules directories + right after init are normal; they fill in when the user opens a fresh session in + this tool. To sync immediately, run `teamai pull`. +4. **Finish with `teamai doctor`.** Every setup or onboarding flow ends by running + it and resolving what it reports before you call the job done. + +Every public command and every flag, including the flags `--help` hides, is listed in +`teamai skill get core --full` under `references/commands.md`. Do not guess a flag: +there is no member-invite flag, for instance — inviting happens on the Git +platform's website, as `manage-admin.md` describes. + +## References + +In the files below, `{SKILL_DIR}` is the directory `teamai skill path setup` prints; a reference file you open on its own writes that directory as `SKILL_DIR` in braces. + +| File | When to load it | +|---|---| +| `{SKILL_DIR}/references/setup-admin.md` | Creating a team repo: provider detection, auth, repo creation, first push. | +| `{SKILL_DIR}/references/join-member.md` | Joining an existing team from a repo URL. | +| `{SKILL_DIR}/references/manage-admin.md` | Day-to-day admin: publishing resources, roles, projects, MCP, env, members. | +| `{SKILL_DIR}/references/uninstall.md` | Removing TeamAI from a machine or from one agent. | +| `{SKILL_DIR}/references/provider-tgit.md` | Tencent TGit: reachability probe, `gf` install and login, repo creation on init. | + +`teamai skill get setup --full` prints this skill with all five appended. diff --git a/skills/teamai/references/join-member.md b/skill-data/setup/references/join-member.md similarity index 80% rename from skills/teamai/references/join-member.md rename to skill-data/setup/references/join-member.md index 5a2929f9..564b2a86 100644 --- a/skills/teamai/references/join-member.md +++ b/skill-data/setup/references/join-member.md @@ -19,7 +19,7 @@ platforms, and — most importantly — **do not create a new repo.** Tell the u *"Ask your team's TeamAI admin for the repository URL, then come back and paste it here."* A member without a repo URL cannot continue; creating one would fork the team into a second, empty repo. (Setting up a brand-new team repo is the admin -flow — see `setup-admin.md` — not this one.) +flow — see `{SKILL_DIR}/references/setup-admin.md` — not this one.) ## Step 1 — Install and verify @@ -39,12 +39,12 @@ If it fails, Node.js ≥ 20 is missing — have them install Node 20+ first. Match the login to the URL's host (do NOT create a second repo): -- **`git.woa.com/...`** (Tencent TGit / 工蜂) → **you run both the `gf` install and +- **`git.woa.com/...`** (Tencent TGit) → **you run both the `gf` install and the `gf … auth login`** (never tell the user to run them). Follow - `provider-tgit.md` ("Log in"); the user's only action is approving the login URL - in their browser / iOA. No `GITLAB_URL` needed. (No headless shortcut: - `TGIT_TOKEN` is REST-API-only and cannot clone, so the login has to be run once - on the machine.) + `{SKILL_DIR}/references/provider-tgit.md` ("Log in"); the user's only action is + approving the login URL in their browser / iOA. No `GITLAB_URL` needed. (No headless + shortcut: `TGIT_TOKEN` is REST-API-only and cannot clone, so the login has to be run + once on the machine.) - **`cnb.cool/...`** → install the CNB CLI, then authorize, in this order: 1. `npm install -g @cnbcool/cnb-cli` 2. `cnb login` — have the user approve it in the browser (OAuth2 device flow); @@ -98,7 +98,8 @@ teamai hooks list # per-tool: which AI tools actually got the hooks Fix anything `doctor` reports. **Don't trust the "Hooks injected into all AI tool settings" message alone** — it prints even for tools where nothing was written; `teamai doctor` / `teamai hooks list` show the real per-tool status. If it flags -hook problems, load `troubleshooting.md` ("Which tools actually get hooks"). +hook problems, load the troubleshooting reference (`"$(teamai skill path core)/references/troubleshooting.md"`), +section "Which tools actually get hooks". ## Step 6 — Confirm the skills actually arrived @@ -123,8 +124,7 @@ tool names only, on a separate branch of that same repo.) ## Agent-specific note If this conversation is running in **ChatGPT App** or **WorkBuddy**, the hooks -that drive auto-sync need an extra manual step — load `troubleshooting.md` -("Agent-specific caveats") and walk the user through it before finishing. +that drive auto-sync need an extra manual step — load the troubleshooting reference (`"$(teamai skill path core)/references/troubleshooting.md"`), section "Agent-specific caveats" and walk the user through it before finishing. ## If something is denied @@ -139,16 +139,18 @@ Summarize the outcome **in the user's own language** (global rule 1). Cover: tool keeps their team skills up to date; no commands needed. 2. **Sharing a session learning is automatic — no command to remember.** When a session produced something worth sharing, TeamAI **prompts them on its own** (at - the end of the session) and the `teamai-share-learnings` skill takes over to + the end of the session) and the `share` workflow (`teamai skill get share`) takes over to summarize and contribute it. They do **not** invoke `/teamai` for this. (This - prompt only appears if the admin left team sharing enabled — it is on by - default; the admin can turn it off in `teamai.yaml`.) + prompt only appears when recall is on — it is off by default; the admin turns it + on in `teamai.yaml` (`sharing.recall.enabled`), a member with `teamai recall enable` — and the admin has not switched the + reminder off in `teamai.yaml`.) 3. **They can also contribute a skill — just ask in plain language.** A member does not need to be an admin to publish a skill. They tell TeamAI something like - *"share this xxx skill with my team"* / *"把这个 xxx skill 分享给团队"*, and you - run the publish for them (see `contribute-member.md`). + *"share this xxx skill with my team"*, in their own language, and you + run the publish for them (see `"$(teamai skill path core)/references/contribute-member.md"`; + it needs no recall). 4. **How to leave — via the skill, not raw commands.** They can remove TeamAI any time by re-invoking the skill; you'll run it for them: - `/teamai 卸载` / `/teamai Uninstall TeamAI`. + `/teamai Uninstall TeamAI` (in their language; the `/teamai` prefix stays as-is). One line, in their language: *"This only removes things from your machine; the team repo stays — rejoin any time with `/teamai` and the repo URL."* diff --git a/skills/teamai/references/manage-admin.md b/skill-data/setup/references/manage-admin.md similarity index 86% rename from skills/teamai/references/manage-admin.md rename to skill-data/setup/references/manage-admin.md index 60e2f87b..c8fe0ae3 100644 --- a/skills/teamai/references/manage-admin.md +++ b/skill-data/setup/references/manage-admin.md @@ -74,7 +74,7 @@ repo per project: ```bash teamai projects list # projects defined + the ones active in this directory -teamai projects set <id> # set the active project(s) for this directory +teamai projects set [ids...] # set the active project(s) for this directory teamai projects members <id> # who is registered on a project ``` @@ -114,21 +114,23 @@ teamai env remove <KEY> # remove ## When sync fails Run `teamai doctor` first. If it reports hook or path problems, load -`troubleshooting.md`. Have the affected member reopen their session; if their tool +the troubleshooting reference (`"$(teamai skill path core)/references/troubleshooting.md"`). Have the affected member reopen their session; if their tool has no session-start hook, they run `teamai pull` manually. ## Capture a lesson learned -Turning a tricky fix into team knowledge is **automatic**: at the end of a session +Turning a tricky fix into team knowledge is **automatic** once recall is on for +the team (`sharing.recall.enabled: true` in `teamai.yaml`, then `teamai push`; it is +off by default, and `teamai recall enable` turns it on for one machine only): at the end of a session worth sharing, TeamAI prompts the member and the dedicated -**`teamai-share-learnings`** skill summarizes the session and runs +`share` workflow (`teamai skill get share`) summarizes the session and runs `teamai contribute`. Nobody has to invoke it by hand. (Publishing a **reusable skill** someone authored is a different task — any member -can do it, see `contribute-member.md`.) +can do it, see `"$(teamai skill path core)/references/contribute-member.md"`.) ### Turn the sharing prompt on or off (admin) -The auto-share prompt is **on by default**. To disable it team-wide, set this in +The auto-share prompt is **on by default once recall is on**. To disable it team-wide, set this in `teamai.yaml` and `teamai push`: ```yaml diff --git a/skills/teamai/references/provider-tgit.md b/skill-data/setup/references/provider-tgit.md similarity index 91% rename from skills/teamai/references/provider-tgit.md rename to skill-data/setup/references/provider-tgit.md index 21114e89..644124c8 100644 --- a/skills/teamai/references/provider-tgit.md +++ b/skill-data/setup/references/provider-tgit.md @@ -1,15 +1,16 @@ -# Provider: Tencent TGit (工蜂) +# Provider: Tencent TGit git.woa.com is **Tencent-internal only**. TeamAI supports it natively as the `tgit` provider — it recognizes the host on its own, so you **never** set `GITLAB_URL`. -Both `setup-admin.md` and `join-member.md` point here for the reachability probe -and the `gf` login; follow the relevant section for whichever flow you are in. +Both `{SKILL_DIR}/references/setup-admin.md` and +`{SKILL_DIR}/references/join-member.md` point here for the reachability probe and +the `gf` login; follow the relevant section for whichever flow you are in. ## Probe reachability (setup flow only) When an admin is choosing a platform and hasn't named one, check whether this machine can reach TGit. A request to git.woa.com that returns the header -`x-env: tgit` means TGit (工蜂) is reachable — plain reachability is not enough, +`x-env: tgit` means TGit is reachable — plain reachability is not enough, the header is what confirms it: ```bash diff --git a/skills/teamai/references/setup-admin.md b/skill-data/setup/references/setup-admin.md similarity index 81% rename from skills/teamai/references/setup-admin.md rename to skill-data/setup/references/setup-admin.md index f75ad458..acabd7b9 100644 --- a/skills/teamai/references/setup-admin.md +++ b/skill-data/setup/references/setup-admin.md @@ -24,13 +24,13 @@ through these sub-steps **in order**: ### 2a — Ask which platform they know -**Tencent-internal first:** before asking, probe whether TGit (工蜂) is reachable -on this machine — see `provider-tgit.md` ("Probe reachability") for the one-line -`x-env: tgit` check. If it says `tgit: OK`, **list Tencent TGit (工蜂) first** and +**Tencent-internal first:** before asking, probe whether TGit is reachable +on this machine — see `{SKILL_DIR}/references/provider-tgit.md` ("Probe +reachability") for the one-line `x-env: tgit` check. If it says `tgit: OK`, **list Tencent TGit first** and prefer it. Then ask: *"Have you heard of / do you have an account on any of these — -Tencent TGit (工蜂), GitHub, GitLab, or CNB (cnb.cool)?"* +Tencent TGit, GitHub, GitLab, or CNB (cnb.cool)?"* -- **Tencent TGit (工蜂)** — https://git.woa.com (Tencent-internal only; shown +- **Tencent TGit** — https://git.woa.com (Tencent-internal only; shown first when the probe above says `tgit: OK`) - **GitHub** — https://github.com - **GitLab** — https://gitlab.com (or a self-hosted company GitLab) @@ -42,7 +42,7 @@ If they name one, use that platform and go to sub-step 2c. Test which sites this network can actually reach (probe each, ~3s timeout each). The TGit probe checks the `x-env: tgit` header, not just reachability (see -`provider-tgit.md`); the others just check reachability: +`{SKILL_DIR}/references/provider-tgit.md`); the others just check reachability: ```bash curl -sS -m 3 -D - -o /dev/null https://git.woa.com 2>/dev/null | grep -qi '^x-env:[[:space:]]*tgit' && echo "tgit: OK" || echo "tgit: unreachable" @@ -51,12 +51,12 @@ curl -sSf -m 3 -o /dev/null https://gitlab.com && echo "gitlab: OK" || echo "g curl -sSf -m 3 -o /dev/null https://cnb.cool && echo "cnb: OK" || echo "cnb: unreachable" ``` -- **TGit reachable (`tgit: OK`)** → prefer Tencent TGit (工蜂); it is the +- **TGit reachable (`tgit: OK`)** → prefer Tencent TGit; it is the Tencent-internal default. - **Exactly one reachable** → use that one. - **Several reachable** → list them (TGit first when present) and let the user pick. - **None reachable** → stop. Tell the user to ask their own admin for a ready-made - repo URL, then switch to `join-member.md`. + repo URL, then switch to `{SKILL_DIR}/references/join-member.md`. Choose by **account + reachability only — never by region**. @@ -67,14 +67,14 @@ the repository, then continue to the next step: | Platform | Sign in / sign up | Create a new repo (do this) | |----------|------------------------------|------------------------------------| -| Tencent TGit (工蜂) | https://git.woa.com | https://git.woa.com/projects/new | +| Tencent TGit | https://git.woa.com | https://git.woa.com/projects/new | | GitHub | https://github.com/login | https://github.com/new | | GitLab | https://gitlab.com/users/sign_in | https://gitlab.com/projects/new | | CNB | https://cnb.cool | https://cnb.cool/new/repos (org first: https://cnb.cool/new/groups) | -> **Tencent TGit (工蜂):** don't send the user to the browser to create the repo — +> **Tencent TGit:** don't send the user to the browser to create the repo — > prefer letting `teamai init` create it via the API in Step 5. See -> `provider-tgit.md` ("When you `teamai init` on TGit"). +> `{SKILL_DIR}/references/provider-tgit.md` ("When you `teamai init` on TGit"). Tell the user to sign in, create an **empty** repo (suggested name `TeamAi-<team-name>`), and give you the resulting repo URL. Explain in one @@ -91,10 +91,10 @@ computer only holds a synced copy — you never put business code in it."* Signing in on the website (Step 2c) is not enough — `teamai init` also needs the platform's CLI credentials. Have the user complete the matching CLI login: -### Tencent TGit (工蜂) +### Tencent TGit -See `provider-tgit.md` ("Log in") — you install `gf` and run `gf auth login` -yourself; the user only approves in the browser / iOA. No `GITLAB_URL` needed. +See `{SKILL_DIR}/references/provider-tgit.md` ("Log in") — you install `gf` and +run `gf auth login` yourself; the user only approves in the browser / iOA. No `GITLAB_URL` needed. Then return here for Step 4. ### CNB — install the CLI, authorize, then read the repo (in this order) @@ -172,9 +172,9 @@ teamai init https://<platform>/<org>/<repo-name> --scope user If the repo does not exist yet, `init` offers to create it — accept the prompt. -- **Tencent TGit (工蜂):** `gf` and login are already done, so init creates the - repo via the API when it's missing — see `provider-tgit.md` ("When you - `teamai init` on TGit"). +- **Tencent TGit:** `gf` and login are already done, so init creates the + repo via the API when it's missing — see + `{SKILL_DIR}/references/provider-tgit.md` ("When you `teamai init` on TGit"). - **CNB caveat:** a `cnb login` token **cannot create** an org or repo — that is exactly why the CNB flow has the user create the repo on the website first (Step 2c). If the org/repo is still missing here, `init` prints web links @@ -200,7 +200,7 @@ Claude Code"). Omitting `--agent` gives an interactive picker — select **every tool already installed** on the machine. Then **report back which agents were set up**, in the user's language: name the tools that will now auto-start TeamAI, and any detected tool that was skipped and why (e.g. Codex trust-gate, -CodeBuddy/WorkBuddy by design — see `troubleshooting.md`). +CodeBuddy/WorkBuddy by design — see the troubleshooting reference, `"$(teamai skill path core)/references/troubleshooting.md"`). ## Step 6 — Verify with doctor @@ -215,8 +215,8 @@ Resolve everything `doctor` flags before continuing. it prints even for tools where nothing was written. `teamai doctor` / `teamai hooks list` show the real per-tool status. Only the tool you set up (e.g. `claude`) is expected to show hooks installed; others are skipped by design or not yet supported, -which is normal. Full table in `troubleshooting.md` ("Which tools actually get -hooks"). +which is normal. Full table in the troubleshooting reference +(`"$(teamai skill path core)/references/troubleshooting.md"`), section "Which tools actually get hooks". ## Step 7 — Grant members repo access (required before they can join) @@ -227,7 +227,7 @@ read/write access to it on the platform website**, or their `teamai init` / `pul Tell the admin (in their language) to add every member on the repo's website: -- **Tencent TGit (工蜂):** repo → 成员管理 / Members → add each member with at +- **Tencent TGit:** repo → Members → add each member with at least **Developer** (read/write) access. - **GitHub:** repo → Settings → Collaborators → add with **Write**. - **GitLab:** repo → Settings → Members → add with **Developer** or above. @@ -249,10 +249,8 @@ carries counts + tool names only, on a separate branch of that same repo.) 1. Give the user their **repo web URL** to share. 2. Give them a ready-to-forward invite line **written in their language**, with the - URL filled in. The `/teamai` prefix stays as-is; translate the rest. For a - Chinese-speaking user, that is: - `/teamai 帮我加入团队的 TeamAI,仓库地址是 <URL>` - (English user: `/teamai Help me join my team's TeamAI, repo URL is <URL>`.) + URL filled in. The `/teamai` prefix stays as-is; translate the rest: + `/teamai Help me join my team's TeamAI, repo URL is <URL>` Tell them to send the URL + this line to each member. 3. Remind them (in their language): **new resources appear only after opening a fresh session** in the AI tool. Right after init the skills folder may look @@ -266,19 +264,18 @@ The user may not be comfortable with the command line, so **don't just hand them list of `teamai …` commands.** Instead, point them back to *this skill* for day-to-day work — they can keep letting the AI run things for them: -- To manage the team later, they run: - `/teamai 我已经装好了,帮我管理` (Chinese) / - `/teamai I already have TeamAI set up, help me manage it` (English) — this loads - the daily-management flow (`manage-admin.md`): publishing skills, inviting +- To manage the team later, they run (in their language): + `/teamai I already have TeamAI set up, help me manage it` — this loads + the daily-management flow (`{SKILL_DIR}/references/manage-admin.md`): publishing skills, inviting members, roles / packages / env. -- To share a reusable skill with the team, they run: - `/teamai 把这个 xxx skill 分享给团队` (Chinese) / - `/teamai Share this <skill-name> skill with my team` (English) — see - `contribute-member.md`. -- Sharing a **session's learnings** is **automatic** — do **not** send them to - `/teamai` for it. TeamAI prompts on its own at the end of a session worth - sharing, and the separate **`teamai-share-learnings`** skill takes over. (Only - when the admin left team sharing on — the default.) +- To share a reusable skill with the team, they run (in their language): + `/teamai Share this <skill-name> skill with my team` — see + `"$(teamai skill path core)/references/contribute-member.md"`. +- Sharing a **session's learnings** needs no command from them: TeamAI prompts on + its own at the end of a session worth sharing, and the `share` workflow + (`teamai skill get share`) takes over. (Only once recall is on — off by default; + turn it on team-wide with `sharing.recall.enabled: true` in `teamai.yaml`, then + `teamai push`.) Mention the underlying commands (`teamai push`, `teamai roles`, …) only as a note for users who *do* want them — the primary path is re-invoking `/teamai`. @@ -289,10 +286,10 @@ Finish by telling the user, **in their language**, that they can remove TeamAI a time — and that they don't need the command line to do it. They just re-invoke the skill and you'll handle it: -`/teamai 卸载` (Chinese) / `/teamai Uninstall TeamAI` (English) +`/teamai Uninstall TeamAI` (in their language; the `/teamai` prefix stays as-is) One line, in their language: *"That removes the hooks and synced resources from your machine; your team repo on the website is untouched — you can rejoin any time with `/teamai` and the repo URL."* -(If they ask right now, load `uninstall.md` and run it for them.) +(If they ask right now, load `{SKILL_DIR}/references/uninstall.md` and run it for them.) diff --git a/skills/teamai/references/uninstall.md b/skill-data/setup/references/uninstall.md similarity index 100% rename from skills/teamai/references/uninstall.md rename to skill-data/setup/references/uninstall.md diff --git a/skill-data/share/SKILL.md b/skill-data/share/SKILL.md new file mode 100644 index 00000000..12fb76c1 --- /dev/null +++ b/skill-data/share/SKILL.md @@ -0,0 +1,70 @@ +--- +name: share +description: >- + Turn a session into a team learning: summarize what was solved, discovered or worked around, + and publish it to the team knowledge base with `teamai contribute`. Loaded on demand by the + teamai discovery stub, and by the friction reminder that ends a session worth sharing. +--- + +# Contribute — share what a session taught you with the team + +Summarize what this AI coding session taught you and push it to the team knowledge base. + +**Write the document in Simplified Chinese**, as earlier releases required. Commands, flags, +URLs, paths and code identifiers stay as they are. + +## When to Use + +- When teamai suggests this session has valuable content worth sharing +- When you've solved a tricky problem and want to document the solution +- When you've discovered a useful workflow or pattern +- After a long session with diverse tool usage + +## How It Works + +1. **Summarize**: review the tools used, the problems solved and the patterns found in this session +2. **Write the document**: a Markdown document (language as above) covering: + - What the task or problem was + - The key decisions and why they were made + - The solution, workaround or pattern discovered + - Which tools or skills proved especially useful + - Pitfalls and things to watch out for +3. **Save it**: write the document to a temporary file +4. **Push it to the team**: run `teamai contribute --file <path> --title "<title>"` + +## Document Template + +Copy the template, the frontmatter field table and the tag taxonomy from +`{SKILL_DIR}/references/doc-template.md`. The frontmatter is required: it is what +makes the document searchable. + +## Example + +```bash +# After writing the summary to /tmp/session-summary.md +teamai contribute --file /tmp/session-summary.md --title "Debugging K8s pod startup timeouts" +``` + +## Important + +- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context +- The document is pushed to the team repo's `teamai-learnings` branch, under `learnings/`, with no pull request +- Team members will see it on their next `teamai pull` +- Keep summaries concise and actionable — this is a knowledge base, not a diary + +## Publishing a reusable skill instead + +A member asking to publish a skill ("share this xxx skill with my team") is a +different flow, and it does not need recall: it lives in the `core` skill, at +`"$(teamai skill path core)/references/contribute-member.md"`. This file +is for turning a *session* into a learning. + +## References + +In the files below, `{SKILL_DIR}` is the directory `teamai skill path share` prints; a reference file you open on its own writes that directory as `SKILL_DIR` in braces. + +| File | When to load it | +|---|---| +| `{SKILL_DIR}/references/doc-template.md` | Writing the learning document: template, frontmatter fields, tag taxonomy. | + +`teamai skill get share --full` prints this skill with its reference appended. diff --git a/skill-data/share/references/doc-template.md b/skill-data/share/references/doc-template.md new file mode 100644 index 00000000..4e3c0c0b --- /dev/null +++ b/skill-data/share/references/doc-template.md @@ -0,0 +1,44 @@ +# Learning document template + +**Required: the document must start with YAML frontmatter.** It feeds the search index +and is how other members discover the learning. + +```markdown +--- +title: "<short title naming the core problem or finding>" +author: <username> +date: <YYYY-MM-DD> +tags: [tag1, tag2, tag3] +--- + +## Context +What were you doing? What problem did you hit? + +## Solution +How did you solve it? What were the key steps? + +## Lessons +- Lesson 1 +- Lesson 2 + +## Related Skills +- skill-name-1 +- skill-name-2 +``` + +### Frontmatter fields + +| Field | Required | Meaning | Example | +|------|------|------|------| +| title | yes | Short title (under 60 characters) | "Diagnosing K8s Pod OOM kills" | +| author | yes | Contributor's username | jeffyxu | +| date | yes | Date as YYYY-MM-DD | 2026-03-28 | +| tags | yes | 2-5 key tags | [k8s, oom, troubleshooting] | + +### Choosing tags + +Pick 2-5 from these categories: +- **Stack**: python, typescript, go, k8s, docker, sglang, cuda +- **Problem type**: troubleshooting, performance, deployment, config, api +- **Pattern**: workflow, pattern, tool-usage, best-practice +- **Scenario**: debugging, testing, monitoring, security diff --git a/skill-data/wiki/SKILL.md b/skill-data/wiki/SKILL.md new file mode 100644 index 00000000..861ebf15 --- /dev/null +++ b/skill-data/wiki/SKILL.md @@ -0,0 +1,314 @@ +--- +name: wiki +description: >- + Make AI truly understand large codebases: for multi-repository, multi-microservice projects + that have evolved over years, run architecture reverse-engineering + a Graph RAG graph + + multi-language AST to compress a huge codebase into a structured knowledge base, where every + conclusion traces back to a code line and every relation carries a confidence label. Suited to + projects with 10+ repositories or microservices that AI cannot understand globally by reading + the code directly. Triggers: architecture analysis, architecture reverse-engineering, + codebase knowledge base, code-to-knowledge, architecture wiki, large multi-repo codebase. + Loaded on demand by the teamai discovery stub. +--- + +# wiki: AI cognition engineering for large codebases + +> Prerequisites: an accessible source directory (multiple repositories supported), Python 3, and an installed teamai CLI. +> The methodology, sub-agent prompts, templates and scripts ship with the CLI. Run `teamai skill path wiki` to get their absolute path; +> `{SKILL_DIR}` in this document refers to that path; a reference file you open on its own writes that directory as `SKILL_DIR` in braces. +> Write the knowledge-base documents in Simplified Chinese, as earlier releases did. When updating an existing knowledge base, keep its file names and headings; `validate_kb.py` accepts both the current English and the earlier Chinese headings. +> The Phase 0 structural baseline uses `teamai codebase --extract`. TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. + +**The problem**: large projects (10+ repositories, dozens of microservices, years of iteration) defeat global understanding by AI. The context window cannot hold all the code, component relations are scattered everywhere, and business rules hide deep in call chains. Letting AI read the code directly is both slow (huge token counts) and inaccurate (no global view). + +**The solution**: use architecture reverse-engineering to systematically compress a huge codebase into a **structured, verifiable, AI-Native** deep knowledge base. Every conclusion traces back to a code line, every relation carries a confidence label, and every update is incrementally verified. AI reads the knowledge base instead of the source, and gains global architecture awareness for about **1/50 of the tokens**. + +## Usage + +The user states the mode in natural language, or simply says "build a codebase knowledge base": + +``` +default Standard: single-session core path +--deep Full K1~K4 + G1~G9 +--update Incremental update of an existing knowledge/ +continue Resume from the _review/progress.json checkpoint +``` + +--- + +## Agent architecture + +| Agent | File | When started | +|-------|------|---------| +| Knowledge base document generator Agent | `{SKILL_DIR}/references/agents/kb-doc-generator.md` | Phase K2, every component batch | +| Graph RAG Agent | `{SKILL_DIR}/references/agents/graph-rag-agent.md` | Phase K3 | + +**Main agent responsibilities**: workflow orchestration, confirmation point management, progress.json maintenance, quality report aggregation. + +--- + +## Entry decision + +**This decision must run first on every activation.** + +``` +IF the user input contains "--update" or "incremental update": + → Update mode +ELSE IF the user input contains "continue" or "resume": + → Continue mode +ELSE: + → Check whether _review/progress.json exists under the user-specified directory + IF it exists → report the state, wait for "resume last run" or "start over" + ELSE → Phase 0 +``` + +--- + +## Continue mode + +``` +Step 1: Locate progress.json +Step 2: Read and parse it, show a resume summary +Step 3: Jump according to current_phase: + "phase0_done" → Phase K1 + "phasek1_waiting_confirm" → Show k1-architecture-map.md, wait for confirmation ① + "phasek1_confirmed" → Phase K2 + "phasek2_batch_N" → Continue Phase K2 from batch N (skip completed ones) + "phasek2_waiting_confirm" → Wait for confirmation ② + "phasek2_confirmed" → Phase K3 + "phasek3_done" → Phase K4 + "phasek4_done"/"completed" → Report completion, ask whether to --update or rerun a component +``` + +--- + +## Update mode (incremental update) + +**Trigger**: the user asks for an "incremental update", or specifies the `--update` mode in this skill. +**Precondition**: an existing progress.json in the completed state. + +``` +Step 1: Read progress.json, get file_hash_cache +Step 2: Scan project_root, compute the current SHA256 of every file +Step 3: Compare hashes, classify: added / modified / deleted +Step 4: Show the change summary, wait for user confirmation: + ┌────────────────────────────────────┐ + │ Change summary │ + │ Added: N files │ + │ Modified: N files (incl. Aurora.py)│ + │ Deleted: N files │ + │ Affected components: [list] │ + │ Affected graph documents: G1/G2/G6/G7 │ + └────────────────────────────────────┘ +Step 5: Rerun only the affected scope: + - Phase K2: regenerate the Type-4 documents of affected components (overwrite) + - Phase K3 partial: update the graph documents that involve changed components (G1/G2/G6/G7) + - Phase K4: rerun validate_kb.py +Step 6: Update file_hash_cache + the metadata.json commit SHA +Step 7: Component-level diff (handle added/removed repositories or components) + IF the repos list differs from last time: + Added repositories → run a full K1 scan on the new repository, add it to the component inventory, generate Type-4 documents + Removed repositories → prepend `⚠️ [DEPRECATED] The repository for this component has been removed` to the component document + → Update the component inventory in k1-architecture-map.md + → Update the G1 matrix (remove rows/columns of removed components, add rows/columns for new ones) +``` + +--- + +## progress.json specification + +**Path**: `<output_dir>/../_review/progress.json` + +```json +{ + "version": "5", + "repos": [ + {"name": "repo-a", "path": "/absolute/path/to/repo-a", "language": "go"}, + {"name": "repo-b", "path": "/absolute/path/to/repo-b", "language": "python"} + ], + "output_dir": "/absolute/path/to/knowledge", + "primary_language": "go", + "project_name": "ProjectName", + "scan_time": "2026-01-01T10:00:00Z", + "current_phase": "phasek2_batch_2", + "confirmed_phases": ["phase0", "phasek1"], + + "service_map": { + "description": "Service name → repository map built in Phase K1 Step 3", + "ServiceA": {"repo": "repo-a", "entry": "cmd/serviceA/main.go"}, + "ServiceB": {"repo": "repo-b", "entry": "app/main.py"} + }, + + "kb_progress": { + "component_total": 12, + "components_done": ["Aurora", "Frame"], + "components_pending": ["CCDB", "Dispatcher"], + "type1_done": false, + "type2_done": false, + "type3_done": false, + "bridge_docs_done": false, + "graph_rag_done": false + }, + + "accuracy_stats": { + "total_claims": 0, + "verified": 0, + "unverified": 0, + "ambiguous_relations": 0 + }, + + "interface_coverage": { + "description": "Interface count reconciliation, filled by the Phase K2 self-check", + "ComponentA": {"type": "HTTP", "scanned": 13, "documented": 0, "gap": 13}, + "ComponentB": {"type": "MQ", "scanned": 5, "documented": 0, "gap": 5} + }, + + "consistency_check": { + "description": "Cross-document consistency check result from Phase K3 Step 3", + "contradictions": 0, + "missing_refs": 0, + "g1_deviations": 0, + "consistency_rate": 0.0 + }, + + "e2e_validation": { + "description": "AI end-to-end validation result from Phase K4 Step 4", + "total_questions": 0, + "correct": 0, + "partial": 0, + "incorrect": 0, + "boundary_ok": 0, + "boundary_fail": 0, + "accuracy_rate": 0.0 + }, + + "file_hash_cache": { + "relative/path/to/file.go": "sha256_hex" + } +} +``` + +> `accuracy_stats` accumulates after every Phase K2 batch and is the global trust indicator of the knowledge base. + +--- + +## Core principles (accuracy first) + +1. **Code is the single source of truth**: every conclusion must cite a code file:line as evidence; anything unverifiable is marked `[UNVERIFIED]` +2. **Three-state confidence is mandatory**: every relation in the graph is labelled `EXTRACTED(1.0)` / `INFERRED(0.6~0.9)` / `AMBIGUOUS(0.1~0.3)`; no invention out of thin air, no 0.5 default +3. **Two-level accuracy verification**: Phase K2 self-checks every document right after generation; Phase K4 verifies the whole knowledge base +4. **Two human-in-the-loop confirmations**: architecture understanding (K①) and component document quality (K②) must be confirmed by a human to stop systematic errors from spreading +5. **Parallel generation + resume from checkpoint**: Type-4 component documents are dispatched in parallel (all Agent calls in the same message); progress.json is persisted after every batch +6. **Token economy**: the `Glob → Grep → Read` three-step method; full directory scans are forbidden +7. **Honest auditing**: `[UNVERIFIED]` must not be hidden; quality numbers are shown in full; when unsure, mark AMBIGUOUS instead of deleting +8. **Cognitive boundary declaration**: the knowledge base README must state explicitly what is covered and what is not, so AI knows when to say "not sure" +9. **Cross-document consistency**: Phase K3 must cross-check relation descriptions between components; contradictions count as "consistent" only after they are fixed +10. **End-to-end verifiable**: Phase K4 tests the knowledge base's actual answering ability with standardised questions; E2E accuracy target ≥ 80% + +--- + +## Phase workflow (loaded on demand) + +The full steps of each phase live in separate files. Load a file when its phase comes up; do not read them all at once: + +| Phase | File | Content | +|---|---|---| +| Phase 0 | `{SKILL_DIR}/references/phases/phase0-init.md` | Initialisation, `teamai codebase --extract` structural baseline, repository inventory | +| Phase K1 | `{SKILL_DIR}/references/phases/k1-reverse-engineering.md` | Architecture reverse-engineering and source material collection, scan script, architecture analysis report | +| Phase K2 | `{SKILL_DIR}/references/phases/k2-documents.md` | Document generation (parallel batches + intermediate quality confirmation) | +| Phase K3 | `{SKILL_DIR}/references/phases/k3-ai-native.md` | AI-Native enhancement + Graph RAG graph document set | +| Phase K4 | `{SKILL_DIR}/references/phases/k4-quality.md` | Quality assessment, validation script, quality report | + +Methodology background (optional, for reference while writing documents): `{SKILL_DIR}/references/methodology/`; +sub-agent prompts: `{SKILL_DIR}/references/agents/`; +knowledge base README template: `{SKILL_DIR}/references/templates/project-overview.md`. + +Human-readable overview (not for execution): `{SKILL_DIR}/references/overview.md`. + +`teamai skill get wiki --full` prints every reference file in one go (about 130 KB). Use it only when you need to read everything. + +## Output directory layout + +``` +<output_dir>/ +├── README.md ← Knowledge base index + retrieval routing rules + cognitive boundary declaration (for AI) +│ Start from the template: cp "{SKILL_DIR}/references/templates/project-overview.md" <output_dir>/README.md +├── {project_name} Technical Architecture.md ← [Type-1] Architecture overview (target ≤80KB, split automatically when larger) +├── {project_name} Technical Architecture-Core Call Chains.md ← [Type-1b] Split out only when Type-1 exceeds 80KB +├── {project_name} Technical Architecture-AI Metadata.md ← [Type-1c] Split out only when Type-1 exceeds 80KB +├── {project_name} Business Architecture.md ← [Type-2] Product capabilities + lifecycle ~70KB +├── {project_name} Deployment Architecture.md ← [Type-3] Deployment topology ~40KB +├── XX_{component}_Design.md × N ← [Type-4] 20~100KB each +├── XX_{project_name}_Core_API_Product_Code_Mapping.md ← [Type-5] Generated only when product docs exist +├── XX_{project_name}_Product_Rules_Cheat_Sheet.md ← [Type-6] +├── XX_{project_name}_Business_Development_SOP.md ← [Type-7] +├── {knowledge_enhancement_doc} × N ← [Type-8] Anti-patterns / RPC contracts / troubleshooting / knowledge library +└── graph/ ← [Type-9] Graph RAG graph document set + ├── README.md ← Graph index + lookup by question type + ├── G1_{project_name}_Component_Dependency_Matrix.md + ├── G2_{project_name}_Component_Call_Chain_Overview.md + ├── G3_{project_name}_Data_Flow_and_Storage_Dependencies.md + ├── G4_{project_name}_Error_Code_Component_Map.md + ├── G5_{project_name}_Cross_Component_Interaction_Scenarios.md + ├── G6_{project_name}_Knowledge_Graph_Triples.md + ├── G7_{project_name}_Architecture_Risks_and_Impact_Analysis.md + ├── G8_{project_name}_Core_Config_Parameter_Index.md + └── G9_{project_name}_Business_Rule_Constraint_Matrix.md + +_review/ ← Process files (not part of the knowledge base) +├── progress.json ← Resume-from-checkpoint + incremental update state +├── metadata.json ← Code baseline version +├── interface-inventory.json ← Interface scan baseline (Phase K1 Step 5) +├── k1-architecture-map.md ← Architecture reverse-engineering result (confirmed by the user) +├── k2-doc-list.md ← Document inventory + accuracy statistics +├── k3-consistency-check.md ← Cross-document consistency check report (Phase K3 Step 3) +└── k4-quality-report.md ← Quality report (incl. E2E validation results) +``` + +--- + +## Control between phases + +| User reply | Behaviour | +|---------|------| +| "continue" / "go on" / "ok" | Enter the next phase | +| "stop" | Stop; files generated so far stay usable | +| Describes a problem directly | Adjust, reconfirm, then continue | +| Edits files directly and then replies "continue" | Continue based on the edited file contents | + +--- + +## Constraints + +- **The main agent does no code analysis**: all of it is done by dedicated Agents; Read the corresponding agent file before starting one +- **No redundant output**: Write generated files directly; never print the full content in the conversation first +- **Component document naming**: `XX_{component}_Design.md` (XX is a two-digit number assigned in dependency-chain order, lower layers get lower numbers) +- **When no product docs exist**: Type-5/6 may be skipped, or constraint values marked `[PRODUCT_DOC_MISSING]`; never guess +- **Parallel mode**: a Type-4 batch must send all Agent calls concurrently in the same message; serial batches run in order + +### Honesty Rules + +- **No invention out of thin air**: every relation in the graph must have an explicit basis in a component document; never guess from names +- **Confidence must not be faked**: EXTRACTED=1.0, INFERRED 0.4~0.9 by evidence strength, AMBIGUOUS 0.1~0.3; the 0.5 default is banned +- **[UNVERIFIED] must not be hidden**: above 20%, add a visible warning at the top of the document +- **Quality numbers shown in full**: validate_kb.py output must not show only the passing items +- **Token cost transparency**: after every batch, show the number of files read and the estimated token consumption +- **When unsure, prefer AMBIGUOUS**: better to mark as pending confirmation than to delete or pretend certainty + +--- + +## Working with the TeamAI CLI (must read) + +| Phase | Command / path | +|------|-------------| +| Phase 0 structural baseline | `teamai codebase --extract <repo> --project <slug>` (writes `<repo>/teamwiki/`) | +| Deep knowledge | Use `teamai codebase --deep-enrich --project <slug> --output <repo>` after extract has written `teamwiki/evidence/code/<slug>/`. `--output` is the repository root, not the `teamwiki/` directory. Prefix with `teamai --dry-run` to preview without writing. TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. | +| Compile into the wiki after K3 | Skip. TeamAI does not ship a separate team-wiki CLI. Continue with this skill using `teamai` and the files under this skill directory. No extra plugin is required. | +| Product docs into the graph | Skip. Same English note as above. | +| Product ↔ code bridging | Use `teamai codebase --reconcile --output <repo>` after product pages and extracted code pages are under `<repo>/teamwiki/`. Prefix with `teamai --dry-run` to preview without updating the graph. | +| One-shot refresh | Use `teamai codebase --extract <repo> --project <slug> --incremental`, reusing the Phase 0 repository path and project slug even when running from another directory. Do not look for another CLI. | +| Quality assessment | Use `python3 "{SKILL_DIR}/scripts/validate_kb.py" <output_dir>` and `teamai codebase --lint --output <repo>` to check `<repo>/teamwiki/` (`--output` takes the repository root, not the `teamwiki/` directory). Skip any extra evaluate binary. | + +**Path convention**: `{SKILL_DIR}` is the directory printed by `teamai skill path wiki`. The methodology is in `{SKILL_DIR}/references/methodology/`, sub-agent prompts in `{SKILL_DIR}/references/agents/`, and scripts in `{SKILL_DIR}/scripts/`. + +The whole workflow runs within the content served by `teamai skill get wiki` and the `teamai` CLI. No extra plugin is required. diff --git a/skill-data/wiki/references/agents/graph-rag-agent.md b/skill-data/wiki/references/agents/graph-rag-agent.md new file mode 100644 index 00000000..b6130fa0 --- /dev/null +++ b/skill-data/wiki/references/agents/graph-rag-agent.md @@ -0,0 +1,344 @@ +# Graph RAG Agent + +## Responsibility + +Extract cross-component relationship information from the generated knowledge base component documents and produce a structured graph document set (G1~G9), solving the information-scattering problem RAG retrieval faces in "cross-component relationship query" scenarios. + +**This agent is started once, serially, by the main agent in Phase K3.** + +## Input package + +``` +all_kb_docs_dir: knowledge base output root directory (contains all Type-1~8 documents) +architecture_map: full content of _review/k1-architecture-map.md +doc_list: _review/k2-doc-list.md (document list) +project_name: project name (used for document naming) +output_dir: graph document output directory (<all_kb_docs_dir>/graph/) +methodology_file: {SKILL_DIR}/references/methodology/phase2-document-types.md, §Type-9 content +``` + +## Execution steps + +### Step 1: Relationship extraction + +Scan all component documents (Type-4) under `all_kb_docs_dir` and extract from the AI Quick Reference table and the body: + +``` +Scan dimensions: +├── Call relationships (upstream component -> this component, this component -> downstream component, communication method) +├── Storage dependencies (which DB/Redis/MQ are read/written) +├── Message topology (published/consumed Exchange/Topic/Queue/RoutingKey) +├── State transitions (operation -> start state -> intermediate state -> final state, state field values) +├── Constraints (operation -> state prerequisites -> hardware constraints -> billing constraints -> quota) +├── Config mapping (config item -> affected behavior -> change risk) +└── Error code ownership (error code range -> component -> troubleshooting direction) +``` + +**Three-state confidence labelling** (every relationship/triple must be labelled, no omissions): + +| Label | Meaning | Evidence basis | Confidence score | +|------|------|---------|-----------| +| `EXTRACTED` | Relationship explicitly described in a component document (e.g. "Upstream component: Aurora(RPC)") | Explicitly recorded in code/docs | 1.0 | +| `INFERRED` | Reasonably inferred relationship (e.g. a dependency chain implied by an architecture diagram) | Structural evidence + reasonable inference | 0.6~0.9 | +| `AMBIGUOUS` | Uncertain relationship, needs manual confirmation | Weak or contradictory evidence | 0.1~0.3 | + +> ⚠️ **Never use 0.5 as a default score**. Evaluate every relationship independently: INFERRED with a direct code reference gets 0.8~0.9, inference based only on naming gets 0.6~0.7, and only genuinely unclear cases use AMBIGUOUS. + +Build intermediate data structures (in memory, do not write files): +- `relations[]`: (from, to, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) +- `state_transitions[]`: (entity, from_state, to_state, trigger_op, state_field_value, **confidence**, **confidence_score**) +- `constraints[]`: (operation, state_req, hardware_req, billing_req, quota_req, **confidence**, **confidence_score**) +- `config_items[]`: (key, default, component, behavior, change_risk, effect_mode) +- `error_codes[]`: (code_range, component, meaning, debug_direction) +- `triples[]`: (subject, predicate, object, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) + +### Step 2: Generate graph documents one by one + +Generate G1~G9 in order (serially, Write each one as soon as it is complete): + +--- + +#### G1: Component Dependency Matrix + +```markdown +# {project_name} Component Dependency Matrix +<!-- search-anchor: component dependencies, dependency matrix, communication method, call relationships --> +## 🤖 AI Quick Reference +| Document scope | Answers the retrieval question "who depends on X? what does X depend on?" | +| Core value | N×N communication matrix + forward/reverse dependency index | +| Use cases | Change impact assessment, service dependency review, architecture refactoring planning | + +## N×N component communication matrix +(rows: caller, columns: callee, values: `RPC`/`MQ`/`DB`/`—`, confidence label in brackets) +Example: `RPC[E]` = EXTRACTED, `MQ[I:0.8]` = INFERRED 0.8, `RPC[A]` = AMBIGUOUS + +## Forward dependency index (what A depends on) +| Component | Depends on | Communication method | Confidence | Typical scenario | + +## Reverse dependency index (who depends on A) +| Component | Depended on by | Communication method | Confidence | Typical scenario | + +## External service dependencies +| External service | Depended on by which components | Communication method | Confidence | Degradation strategy | + +## Confidence statistics +| Label | Count | Notes | +|------|------|------| +| EXTRACTED | N | Directly described in code/docs | +| INFERRED | N | Reasonable inference, scored 0.6~0.9 | +| AMBIGUOUS | N | Uncertain, needs manual confirmation | +``` + +--- + +#### G2: Component Call Chain Overview + state machines + +```markdown +# {project_name} Component Call Chain Overview and State Machines +<!-- search-anchor: call chain, state machine, end-to-end chain, API chain --> +## 🤖 AI Quick Reference +| Document scope | Answers the retrieval question "which modules does API X pass through? how do entity states transition?" | +| Core value | End-to-end chains of core APIs + complete state machines + operation-state constraint matrix | + +## Core API end-to-end call chains +(for each core API, use the standard call chain format + a mermaid sequence diagram) + +## Complete state machines of core entities +(mermaid stateDiagram-v2, annotated with state field values and triggering operations) + +## Operation-state constraint quick matrix +| Operation \ Current state | State A | State B | ... | +(✅ allowed / ❌ forbidden / ⚠️ conditional) + +## AI state-judgement reasoning rules +(mermaid graph TD decision tree) +``` + +--- + +#### G3: Data Flow and Storage Dependencies + +```markdown +# {project_name} Data Flow and Storage Dependencies +<!-- search-anchor: data flow, storage dependencies, MQ topology, cache --> +## Storage system dependency matrix +| Component | MySQL | Redis | MQ | Object storage | Other | + +## MQ queue topology +| Exchange/Topic | Routing Key | Producer | Consumer | Message meaning | + +## Cache strategy matrix +| Component | Cache key pattern | TTL | Invalidation strategy | +``` + +--- + +#### G4: Error Code Component Map + +```markdown +# {project_name} Error Code Component Map +<!-- search-anchor: error code, error mapping, InvalidParameter --> +## Error code range allocation +| Error code range/prefix | Owning component | Meaning scope | + +## External -> internal error code mapping +| External error code | Internal component | Internal meaning | Troubleshooting direction | +``` + +--- + +#### G5: Cross-Component Interaction Scenarios + +For each core business scenario, generate: +```markdown +## Scenario N: {scenario name} +<!-- typical scenarios: create/delete/modify resources, quota checks, billing, state changes, etc. --> +```mermaid +sequenceDiagram + actor User + participant A as {ComponentA} + participant B as {ComponentB} + ... +``` +**Normal flow**: step descriptions +**Exception handling**: each exception branch +``` + +Requirement: >=10 scenarios, covering the main write operations and key read operations. + +--- + +#### G6: Knowledge Graph Triples + +```markdown +# {project_name} Knowledge Graph Triples +<!-- search-anchor: knowledge graph, triples, multi-hop reasoning --> + +## Ontology definition +### Entity types: Service, Handler, Config, Table, Queue, API, ErrorCode +### Relationship types: CALLS, PUBLISHES, CONSUMES, READS, WRITES, CONFIGURES, MAPS_TO + +## Explicit triples (>=100) +| Subject | Predicate | Object | Protocol/Scenario | Confidence | Score | + +> Every triple's Confidence must be `EXTRACTED` / `INFERRED` / `AMBIGUOUS`; Score must not be omitted and must not default to 0.5. + +## Multi-hop dependency path index +| Query pattern | Example path | +| "Which tables does A ultimately write to?" | A→(CALLS)→B→(WRITES)→Table | + +## Reverse reachability index +| Target node | Reachable paths | +``` + +--- + +#### G7: Architecture Risks and Impact Analysis + +```markdown +# {project_name} Architecture Risks and Impact Analysis +<!-- search-anchor: architecture risk, blast radius, impact surface --> +## Component risk level summary +| Component | Risk level | Blast radius | Notes | +(🔴 high / 🟡 medium / 🟢 low) + +## Blast radius analysis of key components (>=3 high-risk components) +Impact chain analysis when component X fails + +## Critical paths and bottleneck identification +## Cluster analysis (which components form tightly coupled clusters) +## Change risk assessment matrix +``` + +--- + +#### G8: Core Config Parameter Index + +```markdown +# {project_name} Core Config Parameter Index +<!-- search-anchor: config parameters, config index, config changes --> +## Layered configuration architecture diagram (mermaid) + +## Config parameter tables per layer +| Config item | Owning component | Default | Affected behavior | Change risk | Effect mode | +(change risk: 🟢 low / 🟡 medium / 🔴 high; effect mode: hot reload / restart required) + +## Config change impact quick reference +| Change type | Impact scope | Effect mode | Rollback strategy | + +## When answering "how do I change config XX", the AI must always state: +1. Config file location +2. Impact scope +3. Effect mode +4. Rollback strategy +5. Change risk +6. Whether a canary rollout is needed +``` + +--- + +#### G9: Business Rule Constraint Matrix + +```markdown +# {project_name} Business Rule Constraint Matrix +<!-- search-anchor: business rules, constraint matrix, operation constraints, AI reasoning --> +## Operation precondition matrix +| Operation | State requirement | Hardware constraint | Billing constraint | Quota constraint | Other constraints | + +## Constraint decision tree (mermaid graph TD) +(covers the multi-layer constraint check flow of the main operations) + +## Special instance type constraint summary +| Instance/resource type | Restricted operations | Reason | +(✅ allowed / ❌ forbidden / ⚠️ conditional) + +## AI reasoning rules quick reference +(mermaid flowchart: the layer-by-layer check order the AI follows when judging "can operation X be performed") +``` + +--- + +### Step 3: Generate the graph directory README + +Write to `{output_dir}/README.md`: +```markdown +# {project_name} Graph Document Set (Graph RAG) +<!-- search-anchor: graph documents, Graph RAG, relationship index --> + +## Relationship to the main document system +(graph documents do not replace component documents; they provide a structured index from the relationship perspective) + +## Document directory +| File | Size | Core content | + +## Look up by question type +| Question type | Example question | Document to consult | +| Dependencies | "Who depends on X?" | G1 Component Dependency Matrix | +| Call chains | "Which modules does API X pass through?" | G2 Call Chain Overview | +| Data location | "Where is the data stored?" | G3 Data Flow and Storage Dependencies | +| Error troubleshooting | "Which module does error code XXX belong to?" | G4 Error Code Component Map | +| Scenario handbook | "What is the full quota check flow?" | G5 Cross-Component Interaction Scenarios | +| Multi-hop reasoning | "What does A indirectly depend on?" | G6 Knowledge Graph Triples | +| Risk assessment | "How big is the impact if X goes down?" | G7 Architecture Risks and Impact Analysis | +| Config changes | "How do I change config XX?" | G8 Core Config Parameter Index | +| Operation constraints | "Can I do XX?" | G9 Business Rule Constraint Matrix | + +## Suggested retrieval routing rules +(keyword -> document to search first) + +## Maintenance notes +(when and how far graph documents must be updated after component documents change) +``` + +### Step 4: Return summary + +``` +Graph RAG generation complete: +Generated documents: G1~G9, 9 in total + README + - G1_Component_Dependency_Matrix.md: {N}KB, {N} components, {N} relationships + Confidence: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} + - G2_Component_Call_Chain_Overview.md: {N}KB, {N} call chains, {N} state machine states + - G3_Data_Flow_and_Storage_Dependencies.md: {N}KB + - G4_Error_Code_Component_Map.md: {N}KB, {N} error code ranges + - G5_Cross_Component_Interaction_Scenarios.md: {N}KB, {N} scenario sequence diagrams + - G6_Knowledge_Graph_Triples.md: {N}KB, {N} triples + Confidence: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} + - G7_Architecture_Risks_and_Impact_Analysis.md: {N}KB + - G8_Core_Config_Parameter_Index.md: {N}KB, {N} config items + - G9_Business_Rule_Constraint_Matrix.md: {N}KB +AMBIGUOUS entries summary (need manual confirmation): {N} places + - Example: "Aurora→Compute communication method uncertain (not specified in docs) [A:0.2]" +Issues found: {issues or "none"} + +⚠️ Note to the main agent: once Graph RAG is complete, immediately run Phase K3 Step 3 (cross-document consistency check). +``` + +## Output + +``` +<output_dir>/README.md +<output_dir>/G1_{project_name}_Component_Dependency_Matrix.md +<output_dir>/G2_{project_name}_Component_Call_Chain_Overview.md +<output_dir>/G3_{project_name}_Data_Flow_and_Storage_Dependencies.md +<output_dir>/G4_{project_name}_Error_Code_Component_Map.md +<output_dir>/G5_{project_name}_Cross_Component_Interaction_Scenarios.md +<output_dir>/G6_{project_name}_Knowledge_Graph_Triples.md +<output_dir>/G7_{project_name}_Architecture_Risks_and_Impact_Analysis.md +<output_dir>/G8_{project_name}_Core_Config_Parameter_Index.md +<output_dir>/G9_{project_name}_Business_Rule_Constraint_Matrix.md +Returned summary string +``` + +## Constraints + +- **Component documents are the sole source for relationship extraction**: do not read the raw code directly, to avoid inconsistency with the Phase K2 output +- **Three-state confidence is mandatory**: every relationship/triple must be labelled `EXTRACTED`/`INFERRED`/`AMBIGUOUS`, no omissions +- **Never use 0.5 as the default confidence**: score every relationship independently; INFERRED with direct structural evidence 0.8~0.9, naming-based inference 0.6~0.7, weak evidence 0.4~0.5; AMBIGUOUS uses 0.1~0.3 +- **Never invent relationships**: if the component documents provide no basis, label it AMBIGUOUS rather than fabricating EXTRACTED +- **Every graph document must have an AI Quick Reference table** +- **Every graph document must have a search-anchor** +- **Graph documents do not replace component documents**: they only provide a structured index from the relationship perspective +- **State machines must use mermaid stateDiagram-v2** +- **Constraint decision trees must use mermaid graph TD** +- **Triples must follow the (Subject, Predicate, Object, Confidence, Score) format** +- **Operation-state constraints must be in ✅/❌/⚠️ matrix format** diff --git a/skill-data/wiki/references/agents/kb-doc-generator.md b/skill-data/wiki/references/agents/kb-doc-generator.md new file mode 100644 index 00000000..9a89c9c0 --- /dev/null +++ b/skill-data/wiki/references/agents/kb-doc-generator.md @@ -0,0 +1,323 @@ +# Knowledge Base Document Generator Agent + +## Responsibility + +Generate knowledge base documents for the assigned batch of components/document types, strictly following the nine document type specifications, ensuring code traceability, complete AI Quick Reference tables, and a web of bidirectional links. + +**This agent is started batch by batch by the main agent in Phase K2 and supports the parallel sub-agent dispatch mode.** + +## Input package + +``` +component_list: list of component names or document types to generate in this batch + e.g. ["Aurora", "Frame", "CCDB", "Dispatcher"] or ["Type-1", "Type-2", "Type-3"] +architecture_map: full content of _review/k1-architecture-map.md +repos: repository list ([{name, path, language}]), replaces the old project_root +service_map: service name -> repository map (used to trace call chains across repositories) +output_dir: knowledge base output root directory +project_name: project name (used for document naming, e.g. "CVM") +product_docs_dir: product documentation directory (may be empty; if empty, skip product constraint extraction) +methodology_dir: {SKILL_DIR}/references/methodology/ directory path +completed_docs: list of already completed documents (skipped when resuming from checkpoint) +parallel_mode: true | false (default true; Type-4 component documents in parallel, Type-1~3/5~8 serially) +``` + +## Execution steps + +### Step 0: Load the methodology + +Read `{methodology_dir}/phase2-document-types.md` and load the templates and generation rules for the relevant document types. + +### Step 1: Checkpoint check + +Check the `completed_docs` list, remove completed items from `component_list`, and obtain `pending_list`. + +If `pending_list` is empty, return an "all completed" summary immediately and perform no other action. + +### Step 2: Dispatch strategy decision + +``` +IF component_list consists only of Type-4 component documents AND parallel_mode = true: + → parallel mode (Step 2A) +ELSE (Type-1/2/3/5/6/7/8 or parallel_mode = false): + → serial mode (Step 2B) +``` + +### Step 2A: Parallel mode (Type-4 component documents) + +**MANDATORY: you must use the Agent tool; processing components one by one in sequence is forbidden.** + +**Step 2A-1: Chunking** + +Split `pending_list` into chunks of **3~5 components** each (component documents are large; do not exceed 5 to avoid context overflow). +- Prefer placing components from the same architecture layer in the same chunk (reduces cross-layer code reading contention) +- Skip completed ones (resume from checkpoint) + +**Step 2A-2: Start all sub-agents concurrently in a single message** + +**Issue all Agent tool calls in the same reply**. This is the only way to run in parallel; issuing them in separate calls degrades to serial execution. + +Example (3 chunks concurrently): +``` +[Agent tool call 1: chunk ["Aurora", "Frame"], subagent_type="general-purpose"] +[Agent tool call 2: chunk ["CCDB", "VSResource"], subagent_type="general-purpose"] +[Agent tool call 3: chunk ["Dispatcher", "Compute"], subagent_type="general-purpose"] +``` + +Each sub-agent receives the following prompt (replace CHUNK_COMPONENTS, CHUNK_NUM, TOTAL_CHUNKS): + +``` +You are the component document generation sub-agent of the wiki skill. +Generate knowledge base documents for the following components (chunk CHUNK_NUM / TOTAL_CHUNKS): +CHUNK_COMPONENTS + +Architecture reference (condensed; only the components in this chunk and their direct upstream/downstream): +RELEVANT_COMPONENTS_TABLE +(format: | Component | Architecture layer | Repository | Language | Upstream | Downstream | Entry file |) + +Service map (for cross-repository tracing): +SERVICE_MAP_RELEVANT_ENTRIES + +Project information: +- repos: REPO_LIST (paths only, no details) +- output_dir: OUTPUT_DIR +- project_name: PROJECT_NAME +- product_docs_dir: PRODUCT_DOCS_DIR (if empty, skip product constraints) + +Methodology path: METHODOLOGY_DIR/phase2-document-types.md + +For each component: +1. Scan the code with the Glob→Grep→Read three-step method (see kb-doc-generator.md §Step 2: Code structure scanning rules) +2. Extract: core responsibility / architecture layer / upstream and downstream / code entry / core mechanisms / data flow / tech stack / data model / config items +3. Generate a document that follows the Type-4 template and Write it to OUTPUT_DIR/XX_{component}_Design.md +4. Self-check (see the Checklist below) +5. Append each completed component name to OUTPUT_DIR/../_review/_chunk_done_CHUNK_NUM.txt (one per line) + +Self-check Checklist (after each document is generated): +- [ ] All 10 dimensions of the AI Quick Reference table filled in and specific (not generic descriptions)? +- [ ] "Code entry" precise to the function name (not just the file name)? +- [ ] search-anchor has 5~15 keywords? +- [ ] Contains a bidirectional link to the main architecture document? +- [ ] Content that cannot be traced is marked [UNVERIFIED]? +- [ ] No empty placeholder sections? + +[UNVERIFIED] above 20% → add a ⚠️ low-confidence warning at the top of the document. + +Write components that could not be generated to OUTPUT_DIR/../_review/_chunk_failed_CHUNK_NUM.txt with the reason. +``` + +**Step 2A-3: Wait and collect results** + +After all sub-agents finish: +- Check the `_chunk_done_N.txt` files to confirm completion status +- If `_chunk_done_N.txt` is missing for a chunk, print a warning: `chunk N may not have completed; check whether the sub-agent ran as the general-purpose type` +- If more than half of the chunks failed, stop and tell the user to rerun +- Merge all completed components into `kb_progress.components_done` in `progress.json` +- Clean up temporary files: `rm -f _review/_chunk_done_*.txt _review/_chunk_failed_*.txt` + +### Step 2B: Serial mode (Type-1~3/5~8) + +For each document type in `pending_list`, execute **in sequence** (these document types depend on each other and must be serial): + +#### 2B-1: Code structure scanning rules + +Use the `Glob → Grep → Read` three-step method (**adapt to the language of the component's repository**): + +``` +1. Glob: find the entry files of the component's repository (choose the pattern by language) + Go: main.go / cmd/*/main.go + Python: main.py / app.py / manage.py / wsgi.py + Java: *Application.java / *Bootstrap.java / src/main/java/**/Main*.java + TypeScript: app.ts / index.ts / main.ts / server.ts + Rust: main.rs / src/main.rs + +2. Grep: locate the core Handlers/Routers (choose the pattern by language + framework) + Go: grep -rn 'func.*Handler\|\.GET\|\.POST\|router\.\|@handler' <dir> + Python: grep -rn '@app\.\|@router\.\|def.*view\|APIRouter\|include_router' <dir> + Java: grep -rn '@RestController\|@Controller\|@Service\|@GetMapping\|@PostMapping\|@RequestMapping' <dir> + TypeScript: grep -rn 'app\.get\|app\.post\|router\.\|@Get\|@Post\|@Controller' <dir> + Rust: grep -rn '\.route\|\.get\|\.post\|#\[get\|#\[post\|async fn' <dir> + + ⚠️ Exclude test files: --exclude='*_test.*' --exclude='test_*' --exclude='*_mock.*' + +3. Read: read the core files (by the directory value rating in architecture_map) + - ⭐⭐⭐ Must read: business logic layer, core config files, DDL + - ⭐⭐ Reference: service context initialisation, config files + - ⭐ Skippable: pure binding layers (usually just parameter pass-through) + - ✗ Forbidden: generated files (*.pb.go, *_gen.go, *_generated.*, node_modules/, target/, build/) +``` + +Extract the following information (**everything must cite a code file:line, no inference**): +- Core responsibility (one sentence, <=30 words) +- Architecture layer and upstream/downstream components (communication method: RPC/MQ/DB) +- Code entry (file name -> core function name) +- Core mechanisms (the 1~2 most important technical mechanisms) +- Data flow (where from -> what it passes through -> where to) +- Tech stack (language + framework + middleware) +- Data model (tables involved + key DDL fields) +- Core flows (the steps needed for sequence diagrams) +- Config items (config key + default value + impact scope) +- Scheduled tasks (if any) +- Monitoring metrics (if any) + +Mark content that cannot be found in the code as `[UNVERIFIED]`; do not infer. + +#### 2B-2: Product documentation extraction (Type-5/6/7, or when product_docs_dir is set) + +If `product_docs_dir` is not empty: +``` +Scan dimensions (from phase2-document-types.md §Type-5 bridge document generation method): +├── Quantity limits (batch caps, quotas, maximums) +├── Type constraints (enum values, mutual exclusions) +├── State preconditions +├── Billing rules +├── Security constraints +└── Compatibility constraints +``` + +Trace every product constraint to its validation location in the code (the exact file:line of the `if len() > N`). + +#### 2B-3: Document generation + +Generate documents following the template for the corresponding type in `phase2-document-types.md`. + +**Type-4 component documents must contain (in order)**: + +```markdown +# {component} Internal Design +<!-- search-anchor: {full name}, {short name}, {abbreviation}, {synonyms}, {common search terms} --> +> Project: {project_name} | Repository: {repo URL} | Architecture layer: {layer} +> Position in the overall architecture: [📘 {project_name} Technical Architecture - 4.X {component}](./{project_name} Technical Architecture.md#4x-component) + +## 🤖 AI Quick Reference +| Dimension | Key information | +|------|---------| +| **Core responsibility** | {<=30 words, specific} | +| **Architecture layer** | {layer name} → {role} | +| **Upstream components** | {ComponentA(RPC)}, {ComponentB(MQ)} | +| **Downstream components** | {ComponentC(RPC)}, {ComponentD(DB)} | +| **Code entry** | `{file name}` → `{core function name}()` | +| **Core mechanisms** | {mechanism 1}; {mechanism 2} | +| **Mutual exclusion** | {concurrency control method, e.g. "distributed lock key: xx"} | +| **Data flow** | {source} → {processing} → {destination} | +| **Tech stack** | {language} + {framework} + {middleware} | +| **Scheduled tasks** | {N scheduled tasks, or "none"} | + +## 📋 Project Overview +(numbered list of core responsibilities + ASCII architecture position diagram) + +## 🏗️ Architecture Design +(ASCII architecture diagram + core sub-module descriptions + core function signatures) + +## 📊 Data Model +(SQL DDL with comments + data flow diagram) + +## 🔌 Interface Design +(external/internal interface tables + error code definitions) + +## ⚙️ Core Flows +(mermaid sequence diagrams + step descriptions + exception handling) + +## 🔧 Configuration +(config item / default value / description / impact scope) + +## 📈 Monitoring and Alerting + +## 🐛 Common Issues and Troubleshooting + +## 📝 Document Change Log +### v1.0 ({date}) +- ✅ **Added**: initial version +> Code baseline: {commit_sha} ({tag}) +``` + +**Write all documents under `output_dir`; printing the full content in the conversation before writing the file is forbidden.** + +### Step 3: Self-check (accuracy verification + interface reconciliation) + +Run after each document is generated; **must not be skipped**: + +**Structural completeness**: +- [ ] All 10 dimensions of the AI Quick Reference table filled in, each with specific information (not "see below")? +- [ ] "Code entry" precise to the function name (`file name:line → function()`)? +- [ ] search-anchor has 5~15 keywords, including full and short names and synonyms? +- [ ] Contains a bidirectional link to the main architecture document? +- [ ] No empty placeholder sections (delete sections with no content)? + +**Interface reconciliation** (only for components whose interface verification type in architecture_map is not NONE): + +Read the component's scanned baseline count `scanned` from `_review/interface-inventory.json` and count the interfaces actually recorded in the document as `documented`: + +``` +HTTP type: count the routes listed in the document's ## Interface Design section +MQ type: count the Topics/Queues/Exchanges explicitly recorded in the document +RPC type: count the RPC Methods listed in the document +``` + +Compute the difference: `gap = scanned - documented` + +Handling rules: +- `gap = 0` → ✅ interface coverage complete +- `0 < gap <= 20%` → ⚠️ minor gap, append `<!-- INTERFACE_GAP: N interfaces possibly missing -->` at the end of the document +- `gap > 20%` → ❌ mark `[INTERFACE_GAP]`, note it in the summary, recommend supplementing and rerunning + +Update the component's `interface_coverage.documented` field in `progress.json`. + +**Accuracy statistics** (computed per document and returned to the main agent for aggregation): +``` +Method: + total_claims = business rule count + core flow step count + interface description count + config item count + verified = those with a file:line reference + unverified = those marked [UNVERIFIED] + ratio = unverified / total_claims +``` + +Handling rules: +- `ratio > 20%` → add `⚠️ Low-confidence warning: {unverified}/{total_claims} items cannot be traced to code` at the top of the document +- `ratio > 40%` → mark **[HIGH_UNVERIFIED]** in the summary and recommend focused manual confirmation + +### Step 4: Return summary + +Return to the main agent (the main agent accumulates the data into `accuracy_stats` and `interface_coverage` in progress.json): + +``` +Batch completion summary: +Files read: {N} (estimated token usage: ~{N}k) +Documents generated: {N} + +Accuracy statistics: + Total claims: {N} | Verified: {N} | [UNVERIFIED]: {N} ({X}%) + +Interface reconciliation (components with interfaces only): + ComponentA [HTTP]: documented {M} / baseline {N} = {X}% ✅/⚠️/❌ + ComponentB [MQ]: documented {M} / baseline {N} = {X}% ✅/⚠️/❌ + +Per-document details: + - {component}_Design.md: {N}KB, {N} claims, [UNVERIFIED] {N} ({X}%) [HIGH_UNVERIFIED/INTERFACE_GAP if applicable] + +Skipped (already completed): {N} +Issues found: {issue description or "none"} +``` + +## Output + +``` +<output_dir>/XX_{component}_Design.md ← Type-4 component document +<output_dir>/{project_name} Technical Architecture.md ← Type-1 (if included in this batch) +<output_dir>/{project_name} Business Architecture.md ← Type-2 +<output_dir>/{project_name} Deployment Architecture.md ← Type-3 +<output_dir>/XX_{project_name}_Core_API_Product_Code_Mapping.md ← Type-5 +<output_dir>/XX_{project_name}_Product_Rules_Cheat_Sheet.md ← Type-6 +<output_dir>/XX_{project_name}_Business_Development_SOP.md ← Type-7 +<output_dir>/{knowledge_enhancement_doc}.md ← Type-8 +Returned summary string +``` + +## Constraints + +- **Code is the truth**: every description must cite a code file; unverifiable content must be marked `[UNVERIFIED]` +- **Templates are mandatory**: read the template for the corresponding section before generating each file type +- **No empty documents**: do not create a file without substantive content +- **No redundant output**: Write files directly; do not print the full content in the conversation +- **Naming convention**: component documents use `XX_{component}_Design.md`; XX is assigned in dependency-chain order (lower-layer components get smaller numbers) +- **When no API is provided**: Type-5/6 may skip the product constraint mapping and mark constraint values as `[PRODUCT_DOC_MISSING]` diff --git a/skill-data/wiki/references/methodology/phase0-collection.md b/skill-data/wiki/references/methodology/phase0-collection.md new file mode 100644 index 00000000..f4139dfa --- /dev/null +++ b/skill-data/wiki/references/methodology/phase0-collection.md @@ -0,0 +1,54 @@ +# Phase 0: Source Material Collection and Preprocessing + +## Repository Discovery and Classification + +Starting from the entry repository, recursively discover all related repositories: + +1. **Dependency analysis**: parse project dependency files (such as `requirements.txt`, `package.json`, `pom.xml`, `Cargo.toml`, `go.mod`, chosen by the detected language) +2. **Configuration references**: parse module names referenced in workflow orchestration configs → repository mapping +3. **RPC service discovery**: extract service names from service registry configs → repository mapping +4. **Classify by architecture layer**: API access layer / workflow engine layer / service execution layer / resource scheduling layer / data adapter layer / base execution layer +5. **Mark core-ness**: compute priority from lines of code, number of dependents, and Handler count + +## Key File Extraction Checklist + +| File type | Match pattern | Extraction purpose | +|---------|---------|---------| +| **Entry files** | `main.py`, `main.go`, `cmd/*/main.go`, `app.ts` | Service startup and initialization flow | +| **Routes/Handlers** | `handler.*`, `router.*`, `controller.*` | API endpoints and message handling entry points | +| **Config files** | `*config*.*`, `conf/`, `*.yaml`, `*.toml` | Workflow orchestration, parameter configuration | +| **Proto/IDL** | `*.proto`, `*.thrift`, `*schema*` | RPC interface contracts and data structures | +| **Database operations** | `*db*.*`, `*dao*.*`, `*model*.*`, `*repository*.*` | Data models and table schemas | +| **Constants/error codes** | `*const*`, `*error*`, `*code*`, `*enum*` | Error code system and business constants | +| **Test files** | `*_test.*`, `test_*.*` | Expected behavior and edge conditions | + +## Building the Code Knowledge Graph + +Before generating documents, build a code knowledge graph as an intermediate representation: + +**Node types**: `[Service]` / `[Handler]` / `[Config]` / `[Table]` / `[Queue]` / `[API]` / `[ErrorCode]` + +**Edge types**: `[CALLS]` (synchronous RPC/HTTP) / `[PUBLISHES]` (asynchronous MQ) / `[CONSUMES]` (MQ consumption) / `[READS]` (DB read) / `[WRITES]` (DB write) / `[CONFIGURES]` (config-driven) / `[MAPS_TO]` (product → code) + +**Construction methods** (ordered by availability): +1. **`teamai codebase --extract`**: Tree-sitter structural edges (**TS/JS/Python/Go** and more) + multi-language heuristic fact pages (writes `teamwiki/`) +2. Grep + Read (Agent K1/K2): supplement dynamic routes and config-driven calls +3. Parse orchestration configs → module → command mapping +4. Parse Proto/IDL/DDL → data structures and table relationships (structured files, can be parsed precisely) +5. MQ topology inference → Exchange/Topic/Queue/Routing Key +6. API mapping → external API name → internal Handler entry point + +> `code-ast` can produce `DEPENDS_ON` edges for relative imports; package-level and dynamic calls may still be missed, mark them `[UNVERIFIED]` or `AMBIGUOUS`. +> AST results take precedence over heuristics. There is no separate capabilities doc in this package; use `teamai codebase --extract` output under `teamwiki/`. + +## Input Source Priority + +| Priority | Input source | Specific content | Output document types | +|--------|--------|---------|------------| +| **P0 required** | Code repositories | Directory structure, entry files, configs, Proto | Type-1,4 | +| **P0 required** | Workflow orchestration configs | workflow_config / state machines | Type-1,4,5 | +| **P0 required** | Product API docs | Interface parameters, error codes | Type-5,6 | +| **P1 important** | Database schema | DDL, table schemas | Type-4 | +| **P1 important** | Product usage docs | Usage limits, FAQ | Type-6,8a | +| **P2 enhancement** | Git history | Commit/MR records | Type-8b | +| **P2 enhancement** | Incident records | Incident reports | Type-8d | diff --git a/skill-data/wiki/references/methodology/phase1-reverse-engineering.md b/skill-data/wiki/references/methodology/phase1-reverse-engineering.md new file mode 100644 index 00000000..a3923530 --- /dev/null +++ b/skill-data/wiki/references/methodology/phase1-reverse-engineering.md @@ -0,0 +1,89 @@ +# Phase 1: Architecture Reverse-Engineering, From Code to Architectural Understanding + +## 1. Bottom-Up Layering Method + +``` +Step 1: Identify "leaf nodes" that operate directly on infrastructure + ├── Database operations (MySQL/PostgreSQL/Redis/MongoDB) + ├── Message queue operations (RabbitMQ/Kafka/RocketMQ) + ├── External system calls (third-party APIs / low-level drivers) + └── File/object storage operations (S3/OSS/COS) + +Step 2: Identify "intermediate nodes" that orchestrate and route + ├── Message routing frameworks (consumer routing and dispatch) + ├── Task schedulers (cron jobs / delayed tasks) + ├── Workflow orchestration engines (Workflow/Saga/state machines) + └── Resource schedulers (load balancing / resource allocation) + +Step 3: Identify "root nodes", the external entry points + ├── API gateway / HTTP Handler / gRPC Server + ├── Scheduled task entry points (Cron/Scheduler) + └── Event listener entry points (Webhook/EventBus) + +Step 4: Layer by call direction + External entry → workflow orchestration → service execution → resource scheduling → data operations → infrastructure +``` + +### Layer Assignment Rules + +| Distinguishing feature | Layer | Typical code pattern | +|---------|---------|-------------| +| HTTP/gRPC Server startup | API access layer | `http.ListenAndServe()`, `grpc.NewServer()` | +| Parameter validation + auth + rate limiting | API access layer | `validate()`, `auth()`, `rateLimit()` | +| Workflow step configs and state machines | Workflow engine layer | `workflow_config`, `state_machine` | +| MQ consumption + Handler routing | Service execution layer | `channel.consume()`, `handler.dispatch()` | +| Scheduling algorithms (Filter/Score) | Resource scheduling layer | `filter()`, `score()`, `schedule()` | +| DB CRUD + cache operations | Data adapter layer | `db.query()`, `redis.get()` | +| Low-level system calls/drivers | Base execution layer | `exec()`, `syscall.*`, `driver.*` | + +## 2. Three-Layer Penetration Tracing (Core Methodology) + +For every user-visible API operation, complete a three-layer penetration trace: + +``` +Layer 1: API entry layer + ├── Locate the Handler function + ├── Extract parameter validation logic + ├── Identify hard-coded defaults and whitelists + └── Determine the downstream call style (synchronous RPC / asynchronous MQ) + +Layer 2: Workflow orchestration layer + ├── Find the workflow config (workflow_config / saga_config) + ├── Parse the step sequence (step name / execution module / rollback module / timeout / retry) + ├── Annotate the execution module and rollback module of each step + └── Determine how data is passed between steps + +Layer 3: Service execution layer + ├── Trace the concrete Handler implementation of each step + ├── Identify database operations and state changes + ├── Annotate external system calls + └── Determine the callback path of the final execution result + +Output: complete call chain sequence diagram + state transition diagram + data flow diagram +``` + +### Standard Format for Documenting Call Chains + +``` +[API name](code entry: {repo}/{path}/{file}) + → parameter validation + auth and rate limiting + → [pre-checks]: {check content} + → RPC/MQ → [orchestration layer] ({config file}: {operation name}) + → [service layer] ({config file}: {flow_name}) + → [{step 1 module}] {step 1 command} ({details}) + → [{step 2 module}] {step 2 command} ({details}) + → ... + → callback to [orchestration layer] +``` + +## 3. Component Relationship Matrix + +Build an N×N relationship matrix annotated with the communication style: + +| Caller ↓ / Callee → | ComponentA | ComponentB | ComponentC | +|---------------------|-------|-------|-------| +| **ComponentA** | — | RPC | MQ | +| **ComponentB** | — | — | DB | +| **ComponentC** | RPC | MQ | — | + +Legend: `RPC` (synchronous) / `MQ` (asynchronous) / `DB` (shared database) / `—` (no direct communication) diff --git a/skill-data/wiki/references/methodology/phase2-document-types.md b/skill-data/wiki/references/methodology/phase2-document-types.md new file mode 100644 index 00000000..28068e0b --- /dev/null +++ b/skill-data/wiki/references/methodology/phase2-document-types.md @@ -0,0 +1,341 @@ +# Phase 2: Generation Specs and Templates for the Nine Document Types + +## Type-1: Technical Architecture Overview + +**Size**: ~200KB | **Count**: 1 + +### Required Sections + +``` +Reader navigation guide (recommended reading paths by role) +Knowledge base retrieval routing guide (AI only, 4 routing rules + 4 priority levels) +1. Architecture overview (30-second quick reference table, overall ASCII architecture diagram, component relationship matrix) +2. Three-dimensional architecture views (logical/data/deployment) +3. Core call chains ⭐ (complete sequence diagram + call chain for every core API) +4. Core components in detail (overview + table per component) +5. Configuration management and service discovery +6. Data model and storage architecture ⭐ +7. High availability and technical architecture +8. Architecture evolution and design decisions +9. AI development knowledge base spec ⭐ (metadata QA / global state machine / MQ topology / scheduling engine / cross-layer tracing) +Appendix: code repositories / glossary / code entry index / error codes +``` + +### Generation Rules +- T1-R01: must include a reader navigation guide +- T1-R02: must include AI retrieval routing rules +- T1-R03: core call chains must have sequence diagrams +- T1-R04: the component table must include a code repository column +- T1-R05: the glossary must include external-to-internal mappings +- T1-R06: must have an AI-only chapter 9 +- T1-R07: architecture diagrams use ASCII Art + +--- + +## Type-2: Business Architecture Document + +**Size**: ~70KB | **Count**: 1 + +``` +1. Product capability matrix (capability domain / sub-capability / corresponding API / billing impact) +2. Billing model in detail (mode comparison / state machine / refund and renewal rules) +3. Core entity lifecycle (complete state machine / operations allowed per state / mutual exclusion rules) +4. Core business flows (user-perspective sequence diagram + preconditions + exception handling) +5. Product specification system (naming rules / mapping from specs to underlying resources) +``` + +--- + +## Type-3: Deployment Architecture Document + +**Size**: ~40KB | **Count**: 1 + +``` +1. Layered deployment architecture diagram +2. Service deployment matrix (service name / deployment method / instance count / resource config / dependencies) +3. Environment configuration (production / test / difference comparison) +4. Deployment process and change management +``` + +--- + +## Type-4: Component Design Document (Core Output) + +**Size**: 20~100KB each | **Count**: N (one per component) + +### Standard Template + +``` +# {component} Internal Design +<!-- search-anchor: component name, aliases, core keywords --> +> Project name / version / code repository / code size +> Position in the overall architecture: [📘 link to the Technical Architecture document] + +## 🤖 AI Quick Reference +(10-dimension structured summary, detailed definition in [phase3-ai-enhancement.md §1](phase3-ai-enhancement.md)) + +## 📋 Project Overview (core responsibilities + position in the architecture) +## 🏗️ Architecture Design (ASCII architecture diagram + core sub-modules, function signatures) +## 📊 Data Model (SQL DDL with comments + data flow diagram) +## 🔌 Interface Design (external interface table + internal interfaces + error codes) +## ⚙️ Core Flows (sequence diagram + step descriptions + exception handling) +## 🔧 Configuration (config item / default / description / impact scope) +## 📈 Monitoring and Alerting +## 🐛 Common Issues and Troubleshooting +``` + +### Generation Rules +- T4-R01: must have an AI Quick Reference table +- T4-R02: must have bidirectional links to the Technical Architecture document +- T4-R03: core functions must list their signatures +- T4-R04: SQL DDL must include comments +- T4-R05: config items must state their impact scope +- T4-R06: architecture diagrams use ASCII Art +- T4-R07: code entries must be precise to the function name + +### Steps for Generating from Code + +> The detailed execution spec is in `{SKILL_DIR}/references/agents/kb-doc-generator.md`; only the outline is listed here: +> 1. Code structure scan (three-step Glob → Grep → Read, adapted per language) +> 2. Information extraction (10 dimensions: core responsibilities / architecture layer / upstream and downstream / code entries / core mechanisms / data flow / tech stack / data model / config items / scheduled tasks) +> 3. Document assembly (in the section order of the template above) +> 4. Self-check (accuracy statistics + interface reconciliation) + +--- + +## Type-5: Product-to-Code Mapping (Bridge Document) + +### One Section per Core API + +``` +### N.1 User intent (one sentence) +### N.2 Product constraints (constraint / value / affected components / validation location) +### N.3 User-visible state transitions (ASCII diagram + internal state mapping) +### N.4 Internal call chain (standard format, precise to the code file) +### N.5 Must-consider items when writing code (numbered list of hard constraints) +### N.6 Error codes and internal exception mapping (external code / internal component / meaning) +``` + +### Generation Rules +- T5-R01: the constraint table must state the "affected components" and "validation location" +- T5-R02: call chains must be precise to the code file path +- T5-R03: state transitions must be annotated with the internal state code mapping +- T5-R04: "Must-consider items when writing code" is a mandatory section +- T5-R05: error code mappings must include the owning internal component + +### Bridge Document Generation Method (3 Steps) + +**Step 1: Extract product constraints**. From the product docs, extract every constraint that affects the code implementation: + +``` +Scan dimensions: +├── Quantity limits (batch caps, quotas, maximums) +├── Type constraints (enum values, mutual exclusion) +├── State preconditions (what state a resource must be in before an operation) +├── Billing rules (different handling per billing mode) +├── Security constraints (auth, encryption, data masking) +└── Compatibility constraints (type compatibility, version compatibility, regional limits) +``` + +**Step 2: Map to code locations**. For each product constraint, trace to the concrete validation location in the code: + +``` +Product constraint: "{API name} batch cap N" + ↓ trace +Code location: {API gateway component} → {file path} → validate_params() + ↓ confirm +Validation: if len(resource_ids) > N: raise InvalidParameterValue +``` + +**Step 3: Build the mapping table**. Assemble the information above into the standard product-to-code mapping table (see the Type-5 template). + +**Bridge document quality criteria**: + +| Quality dimension | Standard | Check method | +|---------|------|---------| +| **Completeness** | Every core API has a mapping | Check one by one against the API list | +| **Precision** | Code paths are precise to file and function | Open the code and verify | +| **Consistency** | Constraint values match the product docs | Cross-check against the product docs | +| **Freshness** | In sync with the latest code version | Periodic diff check | + +--- + +## Type-6: Product Rules Cheat Sheet + +``` +## N. {rule category} +| Rule | Constraint value | Affected components | Validation location | Source document | + +## State and Operation Mutual Exclusion Rules +| Current state | Allowed operations | Forbidden operations | +``` + +- T6-R01: every rule must state the "affected components" +- T6-R02: constraint values must be exact numbers +- T6-R03: must have a "source document" column +- T6-R04: state mutual exclusion rules must be a complete matrix + +--- + +## Type-7: Business Development SOP + +``` +1. Why a standard code template is needed (the problem of unmanaged code) +2. Core conventions (never expose low-level errors externally / pass Context all the way down / validate parameters up front) +3. Standard Handler code template (copy-ready, annotated with "AI coding iron rules") +4. Error code mapping table (scenario described in AI reasoning terms / recommended error code / Message) +5. AI review checklist (machine-checkable) +``` + +- T7-R01: code templates must be directly copyable and runnable +- T7-R02: every key comment is annotated with "AI coding iron rule" +- T7-R03: the error code table uses "the AI's reasoning" as the scenario description + +--- + +## Type-8: Knowledge Enhancement Documents + +### Type-8a: Product Knowledge Library +Marked `type: bridge`; tables compare easily confused concepts and include "code parameter example" and "architecture and business impact" columns. + +### Type-8b: Anti-Patterns and Pitfalls Guide +Five-part structure: **trigger scenario → faulty behavior → root cause analysis → correct approach → related components** +The overview table records number / category / severity (P0 fatal / P1 severe / P2 important) / related components. + +### Type-8c: RPC Interface Contracts +Struct definitions with serialization tags + required/optional markers + AI coding contract requirements. + +### Type-8d: Troubleshooting Case Records (Memorix) +Structure: symptom → investigation process (Step N) → root cause → fix → lessons learned → related documents. + +--- + +## Type-9: Graph Document Set (Graph RAG) + +**Size**: 10~30KB each | **Count**: 5~10 | **Directory**: `graph/` + +> Extracts the **cross-component relationship information** scattered across N component documents into a structured index, solving the "scattered information" problem RAG retrieval hits on relationship queries. + +### Graph Document Type List + +| ID | Document name | Core content | Retrieval pain point solved | +|------|--------|---------|--------------| +| G1 | Component Dependency Matrix | N×N communication matrix + forward/reverse dependency index + external service dependencies | "Who depends on X?" requires traversing every document | +| G2 | Component Call Chain Overview | End-to-end core API chains + read/write separation mechanism + **complete state machine diagram** + operation-state constraint matrix | "Which modules does the API pass through?" information is scattered | +| G3 | Data Flow and Storage Dependencies | Storage dependency matrix + MQ queue topology + cache strategy | "Where is the data stored?" | +| G4 | Error Code Component Map | Error code range allocation + external → internal mapping | "Which module owns this error code?" | +| G5 | Cross-Component Interaction Scenarios | mermaid sequence diagrams for ≥10 scenarios + exception handling | "How is the quota check done?" | +| G6 | Knowledge Graph Triples | (S, P, O) triples + multi-hop dependency path index | "Who does A depend on indirectly?" | +| G7 | Architecture Risks and Impact Analysis | Blast radius + cluster analysis + critical paths/bottlenecks | "How big is the impact if X goes down?" | +| G8 | **Core Config Parameter Index** | Layered config item → behavior impact mapping + change impact surface quick lookup | "How do I change config XX?" | +| G9 | **Business Rule Constraint Matrix** | Operation preconditions + hardware/migration/billing constraints + AI reasoning decision tree | "Can XX be done?" | + +### Graph Document Generation Rules + +- T9-R01: every graph document must have a `🤖 AI Quick Reference` table +- T9-R02: every graph document must have a `<!-- search-anchor: ... -->` anchor +- T9-R03: the graph directory must have a `README.md` index with a "lookup by question type" table and "retrieval routing rule suggestions" +- T9-R04: state machines must use the mermaid `stateDiagram-v2` format +- T9-R05: constraint decision trees must use the mermaid `graph TD` format +- T9-R06: operation-state constraints must be in ✅/❌ matrix format +- T9-R07: config parameters must state "behavior impact", "change risk" (🟢 low / 🟡 medium / 🔴 high), and "activation" (hot reload / restart required) +- T9-R08: business rule constraints must include an AI reasoning check flow (mermaid flowchart) +- T9-R09: triples must follow the standard (Subject, Predicate, Object) format +- T9-R10: graph documents **do not replace** component documents; they provide a **structured index from the relationship perspective** + +### Graph Document Generation Method + +**Step 1: Relationship extraction**. Extract cross-component relationships from the N component documents: + +``` +Scan dimensions: +├── Call relationships (A calls B, protocol, scenario) +├── Data dependencies (A reads/writes B, data content) +├── Message topology (A publishes_to/consumes_from Queue) +├── State transitions (operation → initial state → intermediate state → final state) +├── Constraints (operation → preconditions → hardware/billing/quota constraints) +├── Config mapping (config item → behavior impact → change risk) +└── Error code ownership (error code range → component → investigation direction) +``` + +**Step 2: Structured modeling**. Convert the extracted relationships into standard formats: + +``` +Relationship matrix → N×N table +Call chains → end-to-end text chain + mermaid sequence diagram +State machine → mermaid stateDiagram-v2 +Constraint rules → decision tree (mermaid graph TD) + summary table +Config index → layered table (config item / default / behavior impact / change risk / activation) +Triples → (Subject, Predicate, Object, Protocol, Scenario) table +``` + +**Step 3: Index weaving**. Build cross-references and retrieval routing between the graph documents: + +``` +README.md: +├── Document directory table (file / size / core content) +├── Lookup-by-question-type table (question type / example / document to consult) +└── Retrieval routing rule suggestions (keyword → document to search first) +``` + +### Key Templates + +#### State Machine Diagram Template + +```markdown +## Complete Instance State Machine Diagram + +### Core State Transition Diagram +​```mermaid +stateDiagram-v2 + [*] --> PENDING: CreateAction + PENDING --> RUNNING: creation succeeded (flag: 2→1) + RUNNING --> STOPPING: StopAction (flag: 1→8) + STOPPING --> STOPPED: shutdown succeeded (flag: 8→3) + ... +​``` + +### Operation-State Constraint Quick Lookup Matrix +| Operation \ Current state | RUNNING | STOPPED | PENDING | ... | +|---------------|:-------:|:-------:|:-------:|:---:| +| **Start** | ❌ | ✅ | ❌ | ... | +| **Stop** | ✅ | ❌ | ❌ | ... | +``` + +#### Business Rule Constraint Matrix Template + +```markdown +## Operation Precondition Matrix +| Operation | State requirement | Hardware constraint | Billing constraint | Quota constraint | Other constraints | + +## Migration Constraint Decision Tree +​```mermaid +graph TD + A[Migration request] --> B{Hardware constraint 1?} + B -->|Yes| C["❌ Forbidden"] + B -->|No| D{Hardware constraint 2?} + ... +​``` + +## AI Reasoning Rules Quick Lookup +​```mermaid +graph TD + A["User asks: can XX be executed?"] --> B["Step 1: state check"] + B --> B1{"Look up the operation-state constraint matrix"} + B1 -->|❌| Z1["No, the state does not allow it"] + B1 -->|✅| C["Step 2: type check"] + ... +​``` +``` + +#### Config Parameter Index Template + +```markdown +## {component layer} Config Parameters +| Config item | Default | Behavior impact | Change risk | Activation | +|--------|--------|---------|---------|---------| +| `config.key` | value | description | 🟢 low / 🟡 medium / 🔴 high | hot reload / restart required | + +## Config Change Impact Surface Quick Lookup +| Change type | Impact scope | Activation | Rollback strategy | Change risk | +``` diff --git a/skill-data/wiki/references/methodology/phase3-ai-enhancement.md b/skill-data/wiki/references/methodology/phase3-ai-enhancement.md new file mode 100644 index 00000000..ac0c8841 --- /dev/null +++ b/skill-data/wiki/references/methodology/phase3-ai-enhancement.md @@ -0,0 +1,164 @@ +# Phase 3: AI-Native Enhancement, Making the Knowledge Base Understandable to AI + +## 1. AI Quick Reference Table (required in every component document) + +The chunk returned by RAG retrieval is usually a fragment of a document. The AI Quick Reference table ensures that no matter which part of the document is retrieved, the AI gets the component's global context from the table at the top. + +```markdown +## 🤖 AI Quick Reference + +| Dimension | Key Information | +|------|---------| +| **Core Responsibility** | {one sentence, no more than 30 words} | +| **Architecture Layer** | {layer it belongs to} → {role within that layer} | +| **Upstream Components** | {component (communication method)} | +| **Downstream Components** | {component (communication method)} | +| **Code Entry Point** | {entry file} → {core function} | +| **Core Mechanism** | {the 1-2 most important technical mechanisms} | +| **Mutual Exclusion** | {concurrency control method} | +| **Data Flow** | {where it comes from → what it passes through → where it goes} | +| **Tech Stack** | {language + framework + middleware} | +| **Scheduled Jobs** | {N scheduled jobs (brief description of the core ones)} | +``` + +Rules: +- Every dimension must be **concrete**, never a generic description +- "Code Entry Point" is precise down to `file name → function name` +- "Upstream/Downstream Components" must state the communication method (RPC/MQ/DB) +- The table goes at the very top of the document (immediately after the title) + +## 2. Retrieval Routing Rules (required in the main architecture document) + +Prevents RAG retrieval from "cross-talk" between internal and external documents: + +```markdown +## Knowledge Base Retrieval Routing Guide (AI only) + +### Document Category Overview +| Category | Directory | Document Count | Content Nature | +| [Internal, Bridge] Product-Code Mapping | ... | N docs | Core API intent → constraints → call chain | +| [Internal] Component Design Documents | ... | N docs | Architecture design, code entry points | +| [External] Product API Documentation | ... | N docs | Official API reference | + +### Retrieval Routing Rules +Rule 1, internal architecture first: involves component names / internal concepts → search internal documents only +Rule 2, external documents apply: involves API parameters / product limits → search external documents +Rule 3, mixed queries: involves both → internal first, supplemented by external +Rule 4, check constraints before writing code: the bridge documents must be searched first + +### Document Priority +| Level 1 (core) | Product-Code Mapping + Rules Cheat Sheet | Must check before writing code | +| Level 2 (architecture) | Component Design Documents + main architecture document | Understand internal implementation | +| Level 3 (business) | Business architecture + core call chains | Understand business flows | +| Level 4 (reference) | Raw external API documentation | Only when the above cannot answer | +``` + +## 3. Search Anchor (semantic retrieval anchor) + +Add below the title of every document: + +```html +<!-- search-anchor: keyword1, keyword2, synonym, English term, Chinese term --> +``` + +- Include: Chinese name, English name, abbreviations, synonyms, common search terms +- Count: 5~15 +- Example: `<!-- search-anchor: RPC contract, Schema, interface contract, Protobuf, IDL -->` + +## 4. Bidirectional Link Weaving + +```markdown +# Component document → main architecture document +> Position in the overall architecture: [📘 Technical Architecture - 4.5 {component}](./{project_name} Technical Architecture.md#45-component) + +# Main architecture document → component document +See [{component} Design](./XX_{component}_Design.md) + +# Bridge document → component document +| [{component}](./XX_{component}_Design.md) | Input validation layer | +``` + +Weaving rules: +1. Every component document has ≥ 1 link pointing to the main architecture document +2. Every mention of a component in the main architecture document links to the component document +3. Every component mentioned in a bridge document has a link +4. The "Related Components" of anti-pattern documents have links + +## 5. QA Pair Generation (AI metadata layer) + +Pre-populate high-frequency QA pairs (10~20) in the AI-only section of the main architecture document: + +```markdown +- **Q: How is the state machine of the core entity defined?** + A: See `3.7 Complete Entity State Machine` and `9.2.1 Global State Consistency Mapping Table`. + +- **Q: Where are the workflow steps configured? How are exceptions compensated and rolled back?** + A: N-level orchestration is used. Macro flows are in {config file 1}, fine-grained steps in {config file 2}. + +- **Q: What are the message queue topology and routing rules?** + A: See `9.3.1 MQ Routing Topology`. Core Exchanges/Topics include {list}. + +- **Q: What is the resource mutual exclusion (locking) convention?** + A: See `9.4.4 Distributed Locking and Idempotency Conventions`. {lock scheme} is used. +``` + +Every A must include a concrete section / document reference. + +## 6. Graph Document AI Enhancement Spec + +Graph documents are the **relationship index layer** of an AI-Native knowledge base. They specifically solve retrieval failures of RAG in "cross-component relationship query" scenarios. + +### 6.1 Required Structure of the Graph Document README + +```markdown +# Graph Document Set (Graph RAG) +## Relationship to the Main Document System (three-layer positioning table) +## Document Index (file / size / core content) +## Lookup by Question Type (question type / example / document to consult) +## Suggested Retrieval Routing Rules (keyword → document to search first) +## Maintenance Notes +``` + +### 6.2 Graph Document AI Quick Reference Table + +Every graph document must have this immediately after the title: + +```markdown +## 🤖 AI Quick Reference +| Dimension | Key Information | +|------|---------| +| **Document Positioning** | {one-sentence positioning} | +| **Core Value** | {what the AI can do with this document} | +| **Coverage** | {which entities / relationships are covered} | +| **Usage Scenarios** | {typical example questions} | +| **Relationship to the State Machine** | {if applicable: the state machine solves X, this document solves Y} | +``` + +### 6.3 Embedded AI Reasoning Rules + +Constraint-type graph documents must embed the AI reasoning decision flow: + +```markdown +## AI Reasoning Rules Quick Reference +> When the AI decides "whether an operation can be executed", check layer by layer in this priority order: + +1. **State check** → consult the operation-state constraint matrix +2. **Type check** → consult the special instance type constraint summary +3. **Hardware check** → consult the detailed hardware constraint table +4. **Billing check** → consult the detailed billing constraint table +5. **Quota check** → consult the product rules cheat sheet +6. **Mutual exclusion check** → is there an operation in progress +``` + +### 6.4 Configuration Change Checklist + +For configuration-type graph documents, when the AI answers "how do I change configuration XX" it must also state: + +``` +1. Config file location: which file / repository it lives in +2. Impact scope: all regions, or a single region / single machine +3. Activation method: hot reload, or restart required +4. Rollback strategy: how to roll back quickly +5. Change risk: 🟢 low / 🟡 medium / 🔴 high +6. Canary recommendation: whether a canary release is needed +``` diff --git a/skill-data/wiki/references/methodology/phase4-quality.md b/skill-data/wiki/references/methodology/phase4-quality.md new file mode 100644 index 00000000..c22fd657 --- /dev/null +++ b/skill-data/wiki/references/methodology/phase4-quality.md @@ -0,0 +1,232 @@ +# Phase 4: Quality Assessment and Iterative Improvement + +> Helper tool: `python3 "{SKILL_DIR}/scripts/validate_kb.py" <output_dir>` automatically checks link integrity, anchor coverage, AI Quick Reference table coverage, bidirectional links, and README index inclusion rate + +## Five-Dimension Assessment Model + +| Dimension | Weight | Passing standard | +|------|------|---------| +| **Coverage** | 25% | ≥ 90% of core components are documented | +| **Depth** | 25% | ≥ 80% of code entries can be located directly | +| **Consistency** | 20% | 0 dead links, 0 contradictory descriptions | +| **AI usability** | 20% | RAG retrieval accuracy ≥ 85% | +| **Freshness** | 10% | Core document update lag ≤ 30 days | + +## Coverage Check + +``` +□ Does every code repository have a corresponding component design document? +□ Does every core API have a product-to-code mapping? +□ Does every data table have a schema description in some document? +□ Is every MQ Exchange/Topic/Queue marked in the topology diagram? +□ Is every error code in the mapping table? +□ Is every config item in the configuration description? +□ Is every scheduled task described in some document? +``` + +## RAG Retrieval Test Cases + +| Test type | Example question | Expected hit | +|---------|---------|---------| +| Component location | "Where is the code entry of {component}?" | Component design document | +| Flow tracing | "What is the internal call chain of {API name}?" | Product-to-code mapping | +| Constraint query | "What is the batch cap of {operation}?" | Rules cheat sheet | +| State query | "Which operations can be executed in state {state}?" | State mutual exclusion rules | +| Error investigation | "How do I investigate {error code}?" | Anti-patterns / troubleshooting records | +| Code generation | "Write a Handler for {feature}" | SOP + interface contracts | +| Concept disambiguation | "What is the difference between {A} and {B}?" | Product knowledge library | + +## Incremental Update Trigger Table + +| Trigger condition | Update action | +|---------|---------| +| New code repository | Generate a Type-4 component document | +| API interface change | Update the Type-5 mapping + Type-6 cheat sheet | +| New product feature | Update the Type-2 business architecture + Type-8a knowledge library | +| Production incident | Add a Type-8d troubleshooting record + update Type-8b anti-patterns | +| Architecture refactoring | Update the Type-1 architecture overview + affected Type-4 documents | +| Config change | Update the configuration section of the corresponding component document | + +## Version Management Convention + +Maintain a change log at the bottom of every document: + +```markdown +## 📝 Document Change Log + +### vX.Y (YYYY-MM-DD) +- ✅ **Added**: {description of added content} +- ✅ **Fixed**: {description of fixed content} +- ✅ **Updated**: {description of updated content} +- ⚠️ **Deprecated**: {description of deprecated content} +``` + +## Fixing Common Quality Issues + +| Issue | Fix method | +|------|---------| +| Dead links | Grep `](` links globally, or run `python3 "{SKILL_DIR}/scripts/validate_kb.py" <output_dir>` | +| Inconsistent terminology | Build a glossary and replace globally | +| Outdated code entries | Diff against the code repositories periodically | +| Outdated constraint values | Cross-check against the product docs periodically | +| AI retrieval failures | Add search-anchor keywords | +| Isolated documents | Add bidirectional links | + +--- + +## Complete Generation Pipeline Checklist + +### Phase 0 Checklist: Source Material Collection + +``` +□ All core code repositories cloned +□ Product API docs collected (interface name / inputs / outputs / error codes) +□ Product usage docs collected (usage limits / FAQ / billing description) +□ Database schema extracted (DDL / table schemas) +□ Workflow orchestration configs extracted (workflow_config etc.) +□ Proto/IDL files extracted +□ Error code definitions extracted +``` + +### Phase 1 Checklist: Architecture Reverse-Engineering + +``` +□ Code knowledge graph built (nodes + edges) +□ Architecture layers determined (≥4 layers) +□ Component relationship matrix built (N×N) +□ Core call chains traced (≥5 core APIs) +□ MQ topology inferred (Exchange/Topic/Queue/Routing Key) +□ Database ER model built +□ Glossary compiled (external-to-internal mappings) +``` + +### Phase 2 Checklist: Document Generation + +``` +□ [Type-1] Technical architecture overview document (1) + □ Includes the reader navigation guide + □ Includes AI retrieval routing rules + □ Includes core call chain sequence diagrams (≥5) + □ Includes the component relationship matrix + □ Includes the AI-only chapter 9 + □ Includes the glossary + +□ [Type-2] Business architecture document (1) + □ Includes the product capability matrix + □ Includes the billing model (if applicable) + □ Includes the core entity lifecycle state machine + +□ [Type-3] Deployment architecture document (1) + □ Includes the service deployment matrix + □ Includes environment configuration + +□ [Type-4] Component design documents (N) + □ Each includes an AI Quick Reference table + □ Each includes bidirectional links + □ Each includes code entries (precise to the function) + □ Each includes an architecture diagram (ASCII Art) + □ Each includes core flow descriptions + +□ [Type-5] Product-to-code mapping document + □ Covers all core APIs + □ Each API includes a constraint table + □ Each API includes a call chain + □ Each API includes an error code mapping + +□ [Type-6] Product rules cheat sheet + □ Covers all rule categories + □ Constraint values are exact + □ Includes the state mutual exclusion matrix + +□ [Type-7] Business development SOP + □ Includes runnable code templates + □ Includes the error code mapping table + □ Includes the AI review checklist + +□ [Type-8] Knowledge enhancement documents + □ [8a] Product knowledge library (concept disambiguation) + □ [8b] Anti-patterns and pitfalls guide + □ [8c] RPC interface contracts + □ [8d] Troubleshooting case records +``` + +### Phase 3 Checklist: AI-Native Enhancement + +``` +□ All component documents include an AI Quick Reference table +□ The Technical Architecture document includes retrieval routing rules +□ All documents include a search-anchor +□ Bidirectional link network complete (0 dead links) +□ QA pairs generated (10~20) +□ Document priorities defined +``` + +### Phase 3b Checklist: Graph Document Set (Graph RAG) + +``` +□ [G1] Component Dependency Matrix + □ N×N communication matrix complete + □ Forward/reverse dependency index + □ External service dependencies + +□ [G2] Component Call Chain Overview + state machine + □ End-to-end core API chains (read + write) + □ Complete mermaid state machine diagram + □ Core state field value transition path table (if internal state codes exist) + □ User-visible state ↔ internal state mapping (if multi-layer states exist) + □ Operation-state constraint quick lookup matrix (✅/❌) + □ AI state reasoning rules + +□ [G3] Data Flow and Storage Dependencies + □ Storage system dependency matrix + □ MQ queue topology + □ Cache strategy matrix + +□ [G4] Error Code Component Map + □ Error code range allocation table + □ External → internal error code mapping + +□ [G5] Cross-Component Interaction Scenarios + □ mermaid sequence diagrams for ≥10 scenarios + □ Every scenario has exception handling + +□ [G6] Knowledge Graph Triples + □ Ontology definition (entity types + relationship types) + □ ≥100 explicit triples + □ Multi-hop dependency path index + □ Reverse reachability index + +□ [G7] Architecture Risks and Impact Analysis + □ Component risk level summary table + □ Blast radius analysis (≥3 key components) + □ Cluster analysis + □ Change risk assessment matrix + +□ [G8] Core Config Parameter Index + □ Layered config architecture diagram (mermaid) + □ Config parameter table per layer (config item / default / behavior impact / change risk / activation) + □ Config change impact surface quick lookup matrix + +□ [G9] Business Rule Constraint Matrix + □ Operation precondition matrix + □ Detailed hardware constraint table + □ Migration constraint decision tree (mermaid) + □ Detailed billing constraint table + □ Special instance type constraint summary (✅/❌/⚠️) + □ AI reasoning rules quick lookup (mermaid flowchart) + +□ Graph directory README.md index complete + □ Lookup-by-question-type table + □ Retrieval routing rule suggestions +``` + +### Phase 4 Checklist: Quality Assessment + +``` +□ Coverage ≥ 90% +□ Code entry precision ≥ 80% +□ Dead links = 0 (confirm by running validate_kb.py) +□ RAG retrieval accuracy ≥ 85% +□ Core document update lag ≤ 30 days +□ Terminology consistency check passed +``` diff --git a/skill-data/wiki/references/overview.md b/skill-data/wiki/references/overview.md new file mode 100644 index 00000000..26d2b236 --- /dev/null +++ b/skill-data/wiki/references/overview.md @@ -0,0 +1,124 @@ +# wiki: AI cognition engineering for large codebases + +> TeamAI built-in skill. The methodology, scripts and agent specifications are **not** copied into `.claude/`, `.codebuddy/`, `.cursor/` or any other agent directory: they ship inside the installed CLI and are served on demand by `teamai skill get wiki` (`--full` for the references too). What an agent reads therefore always matches the CLI it is running. `teamai skill path wiki` prints the directory that holds the scripts and templates, for the commands below that run them. TeamAI ships no separate team-wiki CLI, and no extra plugin is required. + +## Why this skill exists + +The AI comprehension problem of large projects: + +| Pain point | Symptom | +|------|---------| +| **Context does not fit** | 10+ repositories and hundreds of thousands of lines of code, far beyond the AI context window | +| **Relations are unclear** | RPC/MQ/DB dependencies between microservices are scattered across repositories with no global view | +| **Rules are not remembered** | Business constraints, state machines and config parameters hide deep in call chains | +| **Answers are inaccurate** | AI sees only local code, lacks global architecture awareness, and hallucinates easily | +| **High token consumption** | Every question re-reads large amounts of source, which is very inefficient | + +## How it is solved + +Architecture reverse-engineering **compresses the huge codebase into a structured knowledge base**: + +- Every conclusion has a code `file:line` as evidence +- Every component relation carries a confidence label (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`) +- Every generation run produces accuracy statistics, with automatic warnings when thresholds are exceeded +- AI reads the knowledge base instead of the source and gains global architecture awareness for **about 1/50 of the tokens** +- In Phase 0, `teamai codebase --extract` can generate evidence-backed structural edges (TS/JS/Python/Go AST + multi-language heuristics) +- After extraction, `teamai codebase --deep-enrich --project <slug> --output <repo>` can generate deterministic graph documents (G1/G2/G3) and deep knowledge; no separate team-wiki CLI is needed + +--- + +## Deliverables + +``` +<output_dir>/ +├── README.md ← Retrieval routing guide (for AI) +├── {project_name} Technical Architecture.md ← Whole-system view, ~200KB +├── {project_name} Business Architecture.md ← Product capabilities + lifecycle +├── {project_name} Deployment Architecture.md ← Deployment topology +├── XX_{component}_Design.md × N ← One per component, with the AI Quick Reference table +├── XX_{project_name}_Core_API_Product_Code_Mapping.md ← Product constraint → code location bridge document +├── XX_{project_name}_Product_Rules_Cheat_Sheet.md +├── XX_{project_name}_Business_Development_SOP.md +├── {anti-patterns / RPC contracts / troubleshooting notes} × N +├── _manifest.json ← Machine-readable manifest (for later graph merging) +└── graph/ ← Graph RAG graph document set + ├── G1 Component dependency matrix + ├── G2 Call chain overview + state machines + ├── G3 Data flow and storage dependencies + ├── G4 Error code component map + ├── G5 Cross-component interaction scenarios (≥10 sequence diagrams) + ├── G6 Knowledge graph triples (≥100 entries, with confidence) + ├── G7 Architecture risks and impact analysis + ├── G8 Core config parameter index + └── G9 Business rule constraint matrix + AI reasoning decision tree +``` + +--- + +## Execution flow + +``` +Phase 0 → Initialisation: collect paths, project name, product doc sources; optional CLI ast+heuristic structural baseline + +Phase K1 → Architecture reverse-engineering: key file extraction → layered analysis → component relation matrix + ⛔ Confirmation point ① Architecture understanding + +Phase K2 → Document generation (parallel batches): + Batches 1~4: Type-4 component documents (dispatched to parallel sub-agents) + ⛔ Confirmation point ② Document quality spot check + Batches 5~7: architecture overview + bridge documents + knowledge enhancement + +Phase K3 → AI-Native enhancement: + search-anchor + bidirectional links + retrieval routing rules + Graph RAG graph document set G1~G9 (three-state confidence labels) + +Phase K4 → Quality assessment: + validate_kb.py automatic checks + Whole-base accuracy audit ([UNVERIFIED] statistics + interface coverage) + Cross-document consistency check (contradiction detection + automatic fixes) + RAG retrieval spot check (7 question types) + AI end-to-end validation (10~15 standard questions + code trace-back) + Quality report generation +``` + +Supports `--update` incremental updates (based on a file hash cache, rerunning only changed components). + +--- + +## File structure + +The files below ship with the CLI; `teamai skill path wiki` prints the directory that contains them (`{SKILL_DIR}` in this document). + +``` +{SKILL_DIR}/ +├── SKILL.md ← Main execution instructions (`teamai skill get wiki`) +├── scripts/ +│ ├── scan_repo.py ← Repository scan helper +│ └── validate_kb.py ← Knowledge base quality validation tool +├── references/ +│ ├── overview.md ← This file +│ ├── agents/ +│ │ ├── kb-doc-generator.md ← Dedicated Agent for Type-1~8 document generation +│ │ └── graph-rag-agent.md ← Dedicated Agent for G1~G9 graph documents +│ ├── methodology/ +│ │ ├── phase0-collection.md ← Source material collection method +│ │ ├── phase1-reverse-engineering.md ← Architecture reverse-engineering method +│ │ ├── phase2-document-types.md ← Specification and quality standards of the nine document types +│ │ ├── phase3-ai-enhancement.md ← AI-Native enhancement method +│ │ └── phase4-quality.md ← Quality assessment checklist +│ ├── phases/ ← Execution steps of each Phase +│ └── templates/ +│ └── project-overview.md ← Knowledge base README template (with cognitive boundary declaration) +``` + +--- + +## Quality standards + +| Dimension | Passing standard | +|------|---------| +| Coverage | ≥90% of P0 core components have documents | +| Accuracy | [UNVERIFIED] < 15% | +| Structural quality | Dead links = 0, search-anchor coverage ≥95% | +| AI usability | RAG retrieval spot check accuracy ≥85% | +| Relation trustworthiness | AMBIGUOUS relations < 10%, all listed for confirmation | diff --git a/skill-data/wiki/references/phases/k1-reverse-engineering.md b/skill-data/wiki/references/phases/k1-reverse-engineering.md new file mode 100644 index 00000000..5db48bb8 --- /dev/null +++ b/skill-data/wiki/references/phases/k1-reverse-engineering.md @@ -0,0 +1,118 @@ +## Phase K1: Architecture Reverse-Engineering and Source Material Collection + +**Methodology**: `{SKILL_DIR}/references/methodology/phase0-collection.md` + `{SKILL_DIR}/references/methodology/phase1-reverse-engineering.md` + +### Step 1: Optionally run the scan script (recommended) + +```bash +python3 "{SKILL_DIR}/scripts/scan_repo.py" <project_root> --depth 2 --top 10 +``` +Output: file statistics + key file discovery report + language distribution. + +### Step 2: Key file extraction + +Scan by priority (see phase0-collection.md for details): +- **P0 required**: entry files, routes/handlers, workflow orchestration config, Proto/IDL +- **P1 important**: database schema (DDL), constant / error code definitions +- **P2 enhancement**: config files, test files (to understand expected behaviour) + +### Step 3: Architecture reverse-engineering (see phase1-reverse-engineering.md for details) + +- Bottom-up layering: leaf nodes (DB/MQ) → intermediate nodes (orchestration/scheduling) → root nodes (API entry points) +- Three-layer penetration tracing: for ≥5 core APIs, complete the full call chain trace API entry → orchestration layer → service execution layer +- Build the N×N component relationship matrix (annotate the communication method: RPC/MQ/DB) + +### Step 4: Generate the architecture analysis report + +Write to `_review/k1-architecture-map.md`: + +```markdown +## Architecture Layers (≥4 layers) +| Layer | Components | Core Responsibility | Code Repository | + +## Component Inventory +| Component | Architecture Layer | **Repository** | Language | Criticality (P0/P1/P2) | Entry File | **Interface Check Type** | + +Interface check type values (ask the user to verify this column at confirmation point ①): + - `HTTP` → API access layer, has HTTP/gRPC route registrations, requires interface count reconciliation + - `MQ` → message processing layer, has MQ Consumer/Exchange declarations, Topic count is the baseline + - `RPC` → internal service layer, has .proto / .thrift / IDL files, Method count is the baseline + - `NONE` → scheduling / execution / data layer, no external interface, no interface count check + +## N×N Component Communication Matrix +(values: RPC/MQ/DB/—, annotated with confidence [E]EXTRACTED/[I]INFERRED/[A]AMBIGUOUS) + +## Core Call Chains (≥5) +(format: API(file:line) → orchestration layer(config:line) → service layer(handler:line) → DB(table)) + +## Glossary +| Internal Term | External / Product Term | Notes | + +## Uncertain Items (for manual confirmation) +(relationships and inferences marked [A], with the reason for the uncertainty) +(components whose interface check type is uncertain, marked [?], to be clarified by the user at confirmation point ①) +``` + +### Step 5: Interface inventory scan (run separately per check type) + +**Run only for components whose interface check type in k1-architecture-map.md is ≠ NONE**: + +``` +FOR each component with interface check type = HTTP: + Run a grep scan: + Go: grep -rn "\.GET\|\.POST\|\.PUT\|\.DELETE\|router\.Handle\|@handler" <component_dir> + Python: grep -rn "@app\.route\|@router\.\|APIRouter\|include_router" <component_dir> + Record: component → HTTP interface count N (SCAN_CONFIDENCE: HIGH/MEDIUM) + +FOR each component with interface check type = MQ: + Run a grep scan: + grep -rn "Exchange\|Queue\|Topic\|consumer\|subscribe\|@KafkaListener" <component_dir> + Record: component → MQ Topic/Queue count N + +FOR each component with interface check type = RPC: + Parse the .proto / .thrift files: + find <component_dir> -name "*.proto" -o -name "*.thrift" | xargs grep "^rpc\|^service" + Record: component → RPC Method count N +``` + +Write the results to `_review/interface-inventory.json`: +```json +{ + "ComponentA": {"type": "HTTP", "count": 13, "confidence": "HIGH"}, + "ComponentB": {"type": "MQ", "count": 5, "confidence": "MEDIUM"}, + "ComponentC": {"type": "RPC", "count": 8, "confidence": "HIGH"}, + "ComponentD": {"type": "NONE", "count": 0, "confidence": "—"} +} +``` + +**When done**: update `current_phase` to `"phasek1_waiting_confirm"`. + +**⛔ Confirmation point ①**: wait for an explicit reply from the user. Do not proceed to the next phase automatically. + +Show the user: +``` +Architecture analysis complete. + +Component inventory (N in total): + P0 core: [list] + P1 important: [list] + P2 auxiliary: [list] + +Interface scan results (for verification): + HTTP interfaces: ComponentA 13, ComponentB 7 + MQ Topics: ComponentC 5 + RPC Methods: ComponentD 8 + Components without interfaces: ComponentE, ComponentF, ... + +AMBIGUOUS relationships (please clarify): + - The communication method of ComponentX → ComponentY is uncertain + +Please confirm (edit k1-architecture-map.md directly, then reply "continue"): + 1. Are the architecture layers and P0/P1/P2 annotations correct? + 2. Is the interface check type (HTTP/MQ/RPC/NONE) of every component accurate? + 3. Are the interface scan counts reasonable? Clearly too few means something was missed; too many may mean test files were scanned. +``` + +After confirmation: update to `"phasek1_confirmed"` → Phase K2. + +--- diff --git a/skill-data/wiki/references/phases/k2-documents.md b/skill-data/wiki/references/phases/k2-documents.md new file mode 100644 index 00000000..5edaa159 --- /dev/null +++ b/skill-data/wiki/references/phases/k2-documents.md @@ -0,0 +1,68 @@ +## Phase K2: Document Generation (batched parallel runs + mid-way quality confirmation) + +**Methodology**: `{SKILL_DIR}/references/methodology/phase2-document-types.md` + +### Generation order (dependency-chain driven, lower layers first) + +``` +Batch 1: data layer + basic execution layer Type-4 component documents ← parallel +Batch 2: resource / scheduling layer Type-4 component documents ← parallel +Batch 3: messaging / service layer Type-4 component documents ← parallel +Batch 4: API entry layer Type-4 component documents ← parallel + ⛔ Confirmation point ② ← manual spot check of component document quality +Batch 5: architecture overview layer (Type-1 + Type-2 + Type-3) ← serial (depends on all layers above being complete) +Batch 6: bridge documents (Type-5 + Type-6 + Type-7) ← serial (depends on product documentation) +Batch 7: knowledge enhancement (Type-8: anti-patterns / RPC contracts / troubleshooting) ← serial +``` + +### Execution flow for each batch + +Read `{SKILL_DIR}/references/agents/kb-doc-generator.md`, assemble the input package and launch: + +``` +component_list: list of components / document types for this batch +architecture_map: full content of _review/k1-architecture-map.md +repos: repository list from _review/repo-manifest.json +service_map: service_map from progress.json +output_dir: <Phase 0> +project_name: <Phase 0> +product_docs_dir: <Phase 0, may be empty> +methodology_dir: {SKILL_DIR}/references/methodology/ +completed_docs: kb_progress.components_done (skipped on resume from checkpoint) +parallel_mode: true (batches 1~4) / false (batches 5~7) +``` + +After each batch completes: +- Append the completed components to `kb_progress.components_done` +- Accumulate `accuracy_stats` (extracted from the self-check summary returned by the Agent) +- Update `current_phase` to `"phasek2_batch_N"` +- Show the token consumption and `[UNVERIFIED]` statistics for this batch + +### ⛔ Confirmation point ② (after batches 1~4 complete) + +Show the user: +``` +{N} component design documents generated. Accuracy statistics: + Total claims: {N} | Verified: {N} | [UNVERIFIED]: {N} ({X}%) + AMBIGUOUS relationships: {N} + +Please spot-check 2~3 documents (the most complex components are recommended): + Path: <output_dir>/XX_<component>_Design.md + +Points to confirm: + 1. Is the code entry point in the AI Quick Reference table precise down to the function name? + 2. Does the core flow description match the actual code? + 3. Is the [UNVERIFIED] ratio acceptable? (<15% recommended) + +If you find a systematic problem, describe it and I will adjust the strategy and regenerate. +``` + +Update `current_phase` to `"phasek2_waiting_confirm"`. +After the user confirms, update to `"phasek2_confirmed"` and continue with batches 5~7. + +### After all batches complete + +Write `_review/k2-doc-list.md` (document list: path + size in KB + [UNVERIFIED] count + generation time). +Update `current_phase` to `"phasek2_done"` → Phase K3. + +--- diff --git a/skill-data/wiki/references/phases/k3-ai-native.md b/skill-data/wiki/references/phases/k3-ai-native.md new file mode 100644 index 00000000..5bb43f91 --- /dev/null +++ b/skill-data/wiki/references/phases/k3-ai-native.md @@ -0,0 +1,121 @@ +## Phase K3: AI-Native Enhancement + Graph Document Set + +**Methodology**: `{SKILL_DIR}/references/methodology/phase3-ai-enhancement.md` + +### Step 1: Inject AI-Native elements + +Add to all generated documents (where the Phase K2 Agent did not add them completely): + +| Element | Requirement | Scope | +|------|------|---------| +| `search-anchor` | 5~15 keywords, first line after the title | All documents | +| AI Quick Reference table | 10 dimensions, immediately after the title | All Type-4 component documents | +| Bidirectional links | component ↔ main architecture, bridge ↔ component | All documents | +| Retrieval routing rules | 4 routing rules + 4 priority levels | Technical architecture overview only | +| QA pairs | 10~20 high-frequency questions + answer references | Chapter 9 of the technical architecture overview only | + +### Step 2: Graph RAG graph document set + +Read `{SKILL_DIR}/references/agents/graph-rag-agent.md`, assemble the input package and launch: + +``` +all_kb_docs_dir: <output_dir> +architecture_map: _review/k1-architecture-map.md +doc_list: _review/k2-doc-list.md +project_name: <Phase 0> +output_dir: <output_dir>/graph/ +methodology_file: {SKILL_DIR}/references/methodology/phase2-document-types.md +``` + +Generate G1~G9 (every relationship carries a mandatory three-state confidence annotation): + +| Graph Document | Question Solved | Confidence Requirement | +|---------|---------|-----------| +| G1 Component Dependency Matrix | "Who depends on X?" | EXTRACTED from explicit document descriptions | +| G2 Call Chain Overview + state machine + constraint matrix | "Which modules does an API pass through?" | call chains EXTRACTED, inferred dependencies INFERRED | +| G3 Data Flow and Storage Dependencies | "Where is the data stored?" | read/write relationships EXTRACTED | +| G4 Error Code Component Map | "Which module does this error code belong to?" | EXTRACTED | +| G5 Cross-Component Interaction Scenarios (≥10 sequence diagrams) | "How is the quota check done?" | sequences EXTRACTED, boundaries INFERRED | +| G6 Knowledge Graph Triples (≥100) | "Who does A depend on indirectly?" | every triple marked E/I/A + score | +| G7 Architecture Risks and Impact Analysis | "How big is the impact if X goes down?" | direct dependencies EXTRACTED, indirect INFERRED | +| G8 Core Config Parameter Index | "How do I change configuration XX?" | EXTRACTED from config files | +| G9 Business Rule Constraint Matrix + AI reasoning decision tree | "Can I do XX?" | rules EXTRACTED, inferences INFERRED | + +Also generate `<output_dir>/graph/README.md` (index + lookup-by-question-type table + retrieval routing suggestions). + +### Step 3: Cross-document consistency check + +**After the Graph RAG Agent finishes, the main agent performs this step itself (do not delegate to a sub-agent).** + +Purpose: detect contradictory descriptions between component documents, preventing inconsistencies such as "A says it calls B over RPC, B says it is called by A over MQ". + +``` +Step 3A: Build the "claim matrix" + + For every Type-4 component document, extract relationship claims from **two levels**: + + Level 1: the "Upstream Components" and "Downstream Components" fields of the AI Quick Reference table + Level 2: call descriptions in the interface design and core flow sections of the body + + If level 1 and level 2 describe the same relationship differently → first record it as an "intra-document contradiction" (a higher-priority problem than header vs body) + + Extraction example: + ComponentX.md header claims: X→Y(RPC), X→Z(MQ) + ComponentX.md body claims: X→Z(HTTP) ← contradicts the header! + ComponentY.md header claims: Y←X(RPC), Y→Z(DB) + ComponentZ.md header claims: Z←X(HTTP), Z←Y(DB) + +Step 3B: Cross-compare + + FOR each pair of components (A, B): + IF A.md claims "A→B over RPC" AND B.md claims "B←A over MQ": + → record contradiction: "A→B communication method inconsistent: A says RPC, B says MQ" + IF A.md claims "A→B" BUT B.md does not mention "called by A": + → record omission: "A claims to call B, but B's document does not mention being called by A" + IF a relationship in the G1 matrix differs from the component document claims: + → record deviation: "G1 matrix says A→B(RPC), but A's document says A→B(MQ)" + +Step 3C: Generate the consistency report + + Write to `_review/k3-consistency-check.md`: + + ```markdown + # Cross-Document Consistency Check Report + + ## Contradictions (must fix) + | Component A | Component B | A's Description | B's Description | Contradiction Type | + |-------|-------|---------|---------|---------| + | X | Z | X→Z(MQ) | Z←X(HTTP) | Communication method inconsistent | + + ## Omissions (recommended additions) + | Claimant | Referenced | Claim | Omission | + |--------|---------|---------|------| + | A | B | A→B(RPC) | B's document does not mention being called by A | + + ## G1 Matrix Deviations (recommended alignment) + | G1 Matrix | Component Document | Deviation | + + ## Statistics + - Contradictions: N (❌ must fix) + - Omissions: N (⚠️ recommended additions) + - G1 deviations: N (⚠️ need alignment) + - Consistent relationships: N (✅) + - Consistency rate: X% + ``` + +Step 3D: Automatic fixes (unambiguous cases only) + + IF contradictions > 0: + FOR each contradiction: + Trace back to the code: use Grep to find the actual call method (e.g. rpc.Call / mq.Publish) + IF the correct side can be determined → fix the description in the wrong side's document + update the G1 matrix + IF it cannot be determined → mark as AMBIGUOUS, leave for the user to confirm at the confirmation point + Recompute the consistency rate after fixing + + IF contradictions = 0: + → skip fixing, go straight to Phase K4 +``` + +**When done**: update `current_phase` to `"phasek3_done"` → Phase K4. + +--- diff --git a/skill-data/wiki/references/phases/k4-quality.md b/skill-data/wiki/references/phases/k4-quality.md new file mode 100644 index 00000000..a2037b9d --- /dev/null +++ b/skill-data/wiki/references/phases/k4-quality.md @@ -0,0 +1,190 @@ +## Phase K4: Knowledge Base Quality Assessment and Report + +**Methodology**: `{SKILL_DIR}/references/methodology/phase4-quality.md` + +### Step 1: Automated validation + +```bash +python3 "{SKILL_DIR}/scripts/validate_kb.py" <output_dir> --verbose +``` + +`--verbose` prints the details of every item (missing anchors, the exact location of dead links). This is exactly the full output required below. + +Output (**must be shown in full, not only the passing items**): +``` +Link integrity: ✅/❌ N dead links +search-anchor: ✅/⚠️ coverage N/M (X%) +AI Quick Reference table: ✅/⚠️ coverage N/M (X%) +Bidirectional links: ✅/⚠️ coverage N/M (X%) +README index: ✅/⚠️ inclusion rate N/M (X%) +``` + +### Step 2: Accuracy audit + +Aggregate the credibility of the whole knowledge base from `accuracy_stats`, and the interface coverage from `interface_coverage`: + +``` +[Content accuracy] +Total claims: N (business rules + interface descriptions + relationships) +Verified (with code reference): N (X%) +[UNVERIFIED]: N (X%) +AMBIGUOUS relationships: N (X%) + +[Interface coverage] (only HTTP/MQ/RPC type components are counted, NONE type is excluded) +HTTP interfaces: documented M / scan baseline N = X% +MQ Topics: documented M / scan baseline N = X% +RPC Methods: documented M / scan baseline N = X% +Overall coverage: X% target ≥ 90% + +⚠️ Interface gap list (components where documented < scan baseline): + - ComponentA: documented 8, scan baseline 13, gap 5 → recommend adding +``` + +⚠️ Manual confirmation list: (documents with [UNVERIFIED] > 20% + components with interface gaps + AMBIGUOUS relationships) + +### Step 3: RAG retrieval spot check + +Following `phase4-quality.md §RAG Retrieval Test Cases`, test 1 question from each of the 7 question types (see the methodology for details) and record the hit rate. + +### Step 4: AI end-to-end validation (E2E Validation) + +**Core idea**: answer a set of standardised questions using the knowledge base, then **trace back to the code to verify the answers**, to detect whether the knowledge base enables the AI to give correct answers. + +``` +Step 4A: Generate the standard validation question set (automatic, based on existing documents) + + **Prefer an external validation set provided by the user**: + IF the user provided a list of validation questions (3~10 real business questions) in Phase 0 or now: + → use the user's questions as the validation set first (source: USER) + → top up automatically to 10~15 questions (source: AUTO) + ELSE: + → generate all automatically (source: AUTO) + + > User-provided questions are more valuable, because when the AI writes its own questions it tends to test areas it already knows, + > and the real blind spots (things the AI did not understand and is unaware of) can only be found by external questions. + + Automatically generate 10~15 validation questions from k1-architecture-map.md and k2-doc-list.md: + + Question type distribution (cover at least the following 5 types): + + ┌────────────────────────────────────────────────────────────────────┐ + │ Type 1: component responsibility (3 questions) │ + │ Pattern: "What is the core responsibility of <component>? Where is the code entry point?" │ + │ Verification: the function / file names in the answer must exist in the code │ + │ │ + │ Type 2: call relationships (3 questions) │ + │ Pattern: "What is the relationship between <component A> and <component B>? How do they communicate?" │ + │ Verification: the answer matches the G1 matrix + the actual imports / calls in the code │ + │ │ + │ Type 3: operation constraints (2 questions) │ + │ Pattern: "Can <operation Y> be executed in <state X>?" │ + │ Verification: the answer matches the G9 constraint matrix + the state checks in the code │ + │ │ + │ Type 4: data flow (2 questions) │ + │ Pattern: "Which tables / queues does <operation Z> ultimately write to?" │ + │ Verification: the answer matches the G3 data flow + the actual SQL / MQ operations in the code │ + │ │ + │ Type 5: error troubleshooting (2 questions) │ + │ Pattern: "What does error code <XXX> mean? Which component produces it?" │ + │ Verification: the answer matches the G4 error code map + the error definitions in the code │ + │ │ + │ Type 6 (optional): knowledge boundary test (2 questions) │ + │ Pattern: deliberately ask about content the knowledge base does not cover (e.g. third-party SDK internals, historical architecture changes) │ + │ Verification: the AI should answer "outside the knowledge base coverage" rather than hallucinate │ + └────────────────────────────────────────────────────────────────────┘ + +Step 4B: Answer using the knowledge base (simulating the AI usage scenario) + + FOR each validation question: + 1. Assume only the knowledge base documents can be read, not the code directly + 2. Find the relevant document following the retrieval routing rules + 3. Extract the answer from the document + +Step 4C: Code trace-back verification + + FOR each answer: + 1. Verify the key claims directly in the code with Grep/Read + 2. Judge the result: + ✅ CORRECT : the answer matches the code + ⚠️ PARTIAL : the answer is partially correct, with omissions or imprecision + ❌ INCORRECT : the answer contradicts the code + 🔇 BOUNDARY_OK : knowledge boundary question, correctly declined to answer (type 6 only) + 🔇 BOUNDARY_FAIL : knowledge boundary question, wrongly gave an answer (type 6 only) + +Step 4D: Write the validation report + + Append to the ## AI End-to-End Validation section of k4-quality-report.md: + + | Question | Type | Retrieved Document | AI Answer Summary | Code Verification | Result | + |------|------|---------|-----------|---------|------| + | Core responsibility of Aurora? | Component responsibility | 03_Aurora_Design.md | Scheduling orchestration... | scheduler.go:42 | ✅ | + | A→B communication method? | Call relationship | G1 matrix | RPC | import rpc_client | ✅ | + | Can operation Y run in state X? | Operation constraint | G9 matrix | No | check_state.go:88 | ✅ | + | Third-party SDK internals? | Knowledge boundary | — | Out of scope | — | 🔇 OK | + + Statistics: + CORRECT: N/M (X%) + PARTIAL: N/M (X%) + INCORRECT: N/M (X%), ❌ every INCORRECT must list the specific contradiction + BOUNDARY_OK: N/N + BOUNDARY_FAIL: N/N + + E2E accuracy = (CORRECT + BOUNDARY_OK) / total questions + Target: ≥ 80% +``` + +**If E2E accuracy < 80%**: list the documents that need improvement and the specific problems in the "Recommendations" section of the quality report. + +### Step 5: Generate the quality report + +Write to `_review/k4-quality-report.md`: + +```markdown +# Knowledge Base Quality Report + +## Overview +- Code baseline: <commit SHA> (<tag>) +- Generated at: <ISO8601> +- Total documents: N (Type-1~8: N, graph G1~G9: 9) + +## Accuracy +| Metric | Value | Status | +| Total claims | N | — | +| With code reference | N (X%) | ✅/❌ | +| [UNVERIFIED] | N (X%) | ✅/<15% / ⚠️15~25% / ❌>25% | +| AMBIGUOUS relationships | N | ✅/⚠️ | + +## Structural Quality (validate_kb.py output) +(shown in full, no numbers hidden) + +## Cross-Document Consistency (summary of k3-consistency-check.md) +| Metric | Value | Status | +| Contradictions | N | ✅=0 / ❌>0 | +| Missing references | N | ⚠️ | +| G1 deviations | N | ⚠️ | +| Consistency rate | X% | target ≥95% | + +## RAG Retrieval Spot Check +| Test Question | Expected Hit | Actual Hit | Result | + +## AI End-to-End Validation +| Metric | Value | Status | +| CORRECT | N/M (X%) | — | +| PARTIAL | N/M (X%) | ⚠️ | +| INCORRECT | N/M (X%) | ❌ | +| BOUNDARY_OK | N/N | ✅ | +| E2E accuracy | X% | target ≥80% | + +INCORRECT details: +(the specific contradiction and improvement suggestion for every INCORRECT) + +## Manual Confirmation List +([UNVERIFIED] over-threshold documents + AMBIGUOUS relationships + contradictions + dead links) + +## Recommendations +(improvement directions based on the consistency check + E2E validation) +``` + +**When done**: update `current_phase` to `"completed"`. The workflow ends. + +--- diff --git a/skill-data/wiki/references/phases/phase0-init.md b/skill-data/wiki/references/phases/phase0-init.md new file mode 100644 index 00000000..19867096 --- /dev/null +++ b/skill-data/wiki/references/phases/phase0-init.md @@ -0,0 +1,112 @@ +## Phase 0: Initialisation + +Ask the user for all of the following in one go (**a single message, not step by step**): + +1. **Paths of all code repositories of the project** (the user lists every repository the project involves): + - Format: one absolute path per line, or comma-separated + - Example: + ``` + /path/to/api-gateway + /path/to/order-service + /path/to/user-service + /path/to/common-lib + ``` + - Note: this is the most critical step. The code of a large project is spread across many repositories, and **all of them** must be provided to build complete architecture awareness. A missing repository = a blind spot in the knowledge base. +2. **Project name** (used in document names, e.g. "CVM", "E-commerce Platform") +3. **Product documentation sources** (optional; when provided, the Type-5/6 bridge documents are generated): + - API documentation directory path + - Usage limits / FAQ document path +4. **Output path** (default: `knowledge/` under the parent directory of the first repository) + +**Step 0A: Repository inventory** + +After receiving the user's repository list, build the repository inventory: + +``` +FOR each path provided by the user: + 1. Verify the path exists and is accessible + 2. Detect whether it is a git repository (has a .git directory) + 3. Detect the primary language (by file extension distribution) + 4. Measure code size (file count + estimated line count) + 5. Record the git commit SHA + tag + +Write the result to _review/repo-manifest.json: +{ + "repos": [ + { + "path": "/absolute/path/to/repo-a", + "name": "repo-a", + "language": "go", + "files": 320, + "lines_estimate": 45000, + "commit": "abc123", + "tag": "v1.2.0", + "accessible": true + }, + ... + ], + "total_repos": N, + "inaccessible": ["path/to/repo-x (permission denied)"] +} +``` + +Show it to the user for confirmation: +``` +Identified {N} repositories: + ✅ repo-a (Go, ~45K lines) + ✅ repo-b (Python, ~12K lines) + ✅ repo-c (Go, ~28K lines) + ❌ repo-x (path does not exist or is not accessible) + +Total: ~{N}K lines of code, {N} repositories +Reply "continue" if this is correct, or add the missing repositories. +``` + +**Step 0B: Auto-detect the primary language** (aggregated over the repository list, does not block the flow): +``` +Detection method: aggregate the file extension distribution of all repositories + .go files dominate → language: "go" + .py files dominate → language: "python" + .java files dominate → language: "java" + .ts/.js files dominate → language: "typescript" + .rs files dominate → language: "rust" + Mixed languages (no clear majority) → language: "mixed" +Note: the language field selects the grep patterns for the interface scan (see Phase K1 Step 5) +``` + +**Step 0C: Record the baseline version**: +```bash +# Record each repository separately +FOR repo in repos: + git -C <repo.path> rev-parse HEAD 2>/dev/null + git -C <repo.path> describe --tags --always 2>/dev/null +``` +Write to `_review/metadata.json`: +```json +{ + "project_name": "CVM", + "scan_time": "<ISO8601>", + "repos": [ + {"name": "repo-a", "commit": "<sha>", "tag": "<tag>"}, + {"name": "repo-b", "commit": "<sha>", "tag": "<tag>"} + ] +} +``` + +**Step 0D: CLI structural baseline (per code repository, recommended)** + +Before the K1 deep read, use TeamAI to extract evidence-backed import/call structural edges (Python/Go/TS etc., `code-ast`) and merge them with the regex baseline (`code-heuristic`): + +```bash +# For each repo. Writes <repo>/teamwiki/ (evidence pages + .indices/graph-index.json). +# Existing flags only: --extract [path], optional --project <slug>, optional --incremental. +teamai codebase --extract <repo_abs_path> --project <project_slug> +``` + +- Output: `teamwiki/evidence/code/<project>/` pages; `teamwiki/.indices/graph-index.json` (structural edges). +- When K1/K2/K3 write `edges[]` in `_manifest.json`: **prefer citing** the `code-ast` edges from extract + their `evidenceRefs` (`path:line`); label Agent inferences `INFERRED`/`AMBIGUOUS`. +- After Phase K3, skip any extra graph compile / merge step that is not a `teamai` command. TeamAI does not ship a separate team-wiki CLI. Continue with this skill using `teamai` and the files under this skill directory. No extra plugin is required. + +Write the initial progress.json (current_phase: "phase0_done") and enter **Phase K1**. + +--- diff --git a/skill-data/wiki/references/templates/project-overview.md b/skill-data/wiki/references/templates/project-overview.md new file mode 100644 index 00000000..653c1dbd --- /dev/null +++ b/skill-data/wiki/references/templates/project-overview.md @@ -0,0 +1,148 @@ +# Knowledge base overview template + +> Used to generate `<output_dir>/README.md`, produced in Phase K2 batch 5 (the top-level index of the knowledge base). + +```markdown +# <Project name>: Deep Knowledge Base +<!-- search-anchor: <project name>, <project English name>, knowledge base, architecture overview, quick navigation, component documents, Graph RAG, graph --> + +> **AI reading guide**: this directory is an AI-Native knowledge base. Read this file first for the global picture and the cognitive boundaries, +> then follow the retrieval routing rules into the relevant document for details. **Never read the whole knowledge base directory at once.** + +## 🤖 Knowledge Base Retrieval Routing Guide (for AI) + +### Quick navigation by question type + +| I want to know... | Read... | Path | +|---------|---------|------| +| Overall system architecture and layering | Technical architecture document | `./{project_name} Technical Architecture.md` | +| Design and implementation of a component | Component design document | `./XX_{component}_Design.md` | +| Dependencies between components | G1 dependency matrix | `./graph/G1_*.md` | +| Which modules an API passes through | G2 call chain overview | `./graph/G2_*.md` | +| Where data lives, MQ topology | G3 data flow | `./graph/G3_*.md` | +| Which module an error code belongs to | G4 error code map | `./graph/G4_*.md` | +| The full flow of a business scenario | G5 interaction scenarios | `./graph/G5_*.md` | +| Who A depends on indirectly (multi-hop query) | G6 knowledge graph triples | `./graph/G6_*.md` | +| Blast radius if component X goes down | G7 risk analysis | `./graph/G7_*.md` | +| How to change a configuration | G8 config parameter index | `./graph/G8_*.md` | +| Whether an operation is allowed | G9 business rule constraints | `./graph/G9_*.md` | +| Product constraint → code location mapping | Core API mapping document | `./XX_*_Core_API_Product_Code_Mapping.md` | +| Business development SOP | Business development guidelines | `./XX_*_Business_Development_SOP.md` | + +### Retrieval rules + +- **Rule 1, index first, then dig in**: for an unfamiliar component, read this file first to find the right path, then go into the component document +- **Rule 2, component-internal questions go to the component document**: core mechanisms, code entry points, data models → `XX_{component}_Design.md` +- **Rule 3, cross-component relation questions go to the graph**: dependency matrix, call chains, impact surface → the `graph/` directory +- **Rule 4, operation feasibility questions go to G9**: constraint matrix + decision tree → `graph/G9_*.md` +- **Rule 5, content marked `[UNVERIFIED]` must not be used for code generation** until confirmed by a human +- **Rule 6, `AMBIGUOUS` relations must not be used for change impact assessment** until clarified + +--- + +## 🚧 Cognitive Boundary Declaration (AI must read) + +> This section declares what this knowledge base **does not know**. When a question touches the areas below, the AI +> **must proactively tell the user "this information is outside the knowledge base coverage; check the source code / product docs / contact the team"** +> instead of trying to infer or hallucinate. + +### Coverage + +| Dimension | Coverage | Notes | +|------|------|------| +| Code baseline | `<commit SHA>` (`<tag>`) | Changes **after** this version are not covered | +| Generated at | `<YYYY-MM-DDTHH:MM:SSZ>` | Time anchor between the knowledge base and the code | +| Core components (P0) | <P0 component list> | Deepest documentation, interface-level coverage | +| Important components (P1) | <P1 component list> | Medium documentation depth, core mechanisms covered | +| Auxiliary components (P2) | <P2 component list> | Limited documentation depth, architecture level only | + +### Explicitly not covered (AI should not attempt to answer) + +| Area | Reason | +|------|------| +| Internals of third-party SDKs/libraries | The knowledge base records only how they are called, not third-party source | +| Operations/deployment details (ansible/k8s config) | Outside the scope of a codebase knowledge base; consult the operations docs | +| Non-code deliverables (UI design, original product PRDs) | Only the Type-5/6 bridge documents map product constraints | +| Historical architecture evolution | Only the architecture of the current code baseline is reflected | +| Performance benchmark data | The knowledge base contains no load-test data | +| <project-specific uncovered items> | <reason> | + +### Low-confidence areas (extra warning needed when answering) + +| Area | Reason | Recommendation | +|------|------|------| +| Internal details of P2 auxiliary components | Limited documentation depth | Add "based on limited documentation analysis" when citing | +| Content marked `[UNVERIFIED]` | Cannot be traced back to code | Must tell the user "this information is not verified against code" | +| `AMBIGUOUS` relations | Confidence < 0.3 | Must tell the user "this relation is uncertain" | +| Type-5/6 when product docs are missing | No product doc input | Marked `[PRODUCT_DOC_MISSING]` | + +### Knowledge base update notes + +- **Incremental update**: `teamai codebase --extract <repo> --project <slug> --incremental` re-extracts only the changed files +- **Full rebuild**: recommended after large-scale code refactoring +- **Last updated**: `<ISO8601>` + +--- + +## Project introduction + +<!-- 1-3 sentences: project background, core business goals, main users --> + +## Tech stack + +| Category | Technology | Notes | +|------|------|------| +| Language | Go / Python | ... | +| Framework | go-zero / FastAPI | ... | +| Database | MySQL / PostgreSQL | ... | +| Cache | Redis | ... | +| Message queue | Kafka / RabbitMQ | (if any) | + +## Knowledge base document index + +### Architecture-level documents +| Document | Type | Size | Notes | +|------|------|------|------| +| {project_name} Technical Architecture.md | Type-1 | ~200KB | Architecture overview | +| {project_name} Business Architecture.md | Type-2 | ~70KB | Product capabilities + lifecycle | +| {project_name} Deployment Architecture.md | Type-3 | ~40KB | Deployment topology | + +### Component design documents +| No. | Component | Layer | Priority | Size | +|------|------|--------|--------|------| +| 01 | <component> | <layer> | P0 | ~NKB | + +### Bridge documents (generated when product docs exist) +| Document | Type | Notes | +|------|------|------| +| Core API Product Code Mapping | Type-5 | Product constraint → code location | +| Product Rules Cheat Sheet | Type-6 | Usage limits / FAQ → code | +| Business Development SOP | Type-7 | Development / change operation guidelines | + +### Graph document set (Graph RAG) +| Document | Purpose | Size | +|------|------|------| +| G1~G9 | Cross-component relation index | See `graph/README.md` | + +## Knowledge base quality overview + +| Metric | Value | Status | +|------|------|------| +| Total documents | N | - | +| Content accuracy (with code references) | X% | ✅/⚠️ | +| [UNVERIFIED] ratio | X% | Target <15% | +| Interface coverage (non-NONE components) | X% | Target ≥90% | +| AMBIGUOUS relation count | N | Needs human confirmation | + +> See `_review/k4-quality-report.md` for the detailed quality report + +## Code baseline version + +> ⚠️ This knowledge base was generated from the code version below. After the code evolves, run `teamai codebase --extract <repo> --project <slug> --incremental` for an incremental update. + +- **Commit**: `<git commit SHA>` +- **Tag**: `<tag or "no tag">` +- **Generated at**: `<YYYY-MM-DDTHH:MM:SSZ>` + +> Version information source: `_review/metadata.json` +``` diff --git a/skills/team-wiki-codebase/scripts/scan_repo.py b/skill-data/wiki/scripts/scan_repo.py similarity index 68% rename from skills/team-wiki-codebase/scripts/scan_repo.py rename to skill-data/wiki/scripts/scan_repo.py index b75ad28c..13e54c22 100644 --- a/skills/team-wiki-codebase/scripts/scan_repo.py +++ b/skill-data/wiki/scripts/scan_repo.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 """ -scan_repo.py — 代码仓库结构扫描与统计工具 +scan_repo.py: repository structure scan and statistics tool -用途: Phase 0 源材料采集阶段,快速扫描目标仓库/目录,输出: - 1. 目录结构树(2层深度) - 2. 代码统计(语言分布、文件数、总行数) - 3. 关键文件发现(入口文件、配置文件、Proto/IDL、错误码定义) - 4. 代码热点(文件行数 Top 20) +Purpose: in the Phase 0 source material collection stage, quickly scan the target repository/directory and print: + 1. Directory tree (2 levels deep) + 2. Code statistics (language distribution, file count, total lines) + 3. Key file discovery (entry files, config files, Proto/IDL, error code definitions) + 4. Code hotspots (top 20 files by line count) -使用方式: +Usage: python3 scan_repo.py /path/to/repo python3 scan_repo.py /path/to/repo --depth 3 --top 30 """ @@ -19,38 +19,38 @@ from pathlib import Path from collections import defaultdict, Counter -# 关键文件匹配模式 +# Key file match patterns KEY_FILE_PATTERNS = { - "入口文件": [ + "Entry files": [ "main.py", "main.go", "app.py", "app.ts", "app.js", "server.py", "server.go", "wsgi.py", "manage.py", "cmd/*/main.go", "index.ts", "index.js", ], - "路由/Handler": [ + "Routes/Handlers": [ "*handler*", "*router*", "*controller*", "*dispatch*", "*route*", "*api.*", "*endpoint*", ], - "配置文件": [ + "Config files": [ "*.yaml", "*.yml", "*.toml", "*.ini", "*.conf", "*config*", "*.env", "*.env.*", ], "Proto/IDL": [ "*.proto", "*.thrift", "*.graphql", "*schema*", ], - "数据库/模型": [ + "Database/Models": [ "*model*", "*dao*", "*repository*", "*migration*", "*schema*", "*.sql", "*db*", ], - "常量/错误码": [ + "Constants/Error codes": [ "*const*", "*constant*", "*error*", "*code*", "*enum*", "*define*", "*exception*", ], - "测试文件": [ + "Test files": [ "*_test.*", "test_*", "*.spec.*", "*_spec.*", ], } -# 语言扩展名映射 +# Language extension map LANG_MAP = { ".py": "Python", ".go": "Go", ".js": "JavaScript", ".ts": "TypeScript", ".java": "Java", ".rs": "Rust", ".rb": "Ruby", ".php": "PHP", @@ -61,7 +61,7 @@ ".json": "JSON", ".xml": "XML", ".md": "Markdown", } -# 忽略目录 +# Ignored directories IGNORE_DIRS = { ".git", ".svn", "node_modules", "__pycache__", ".tox", ".mypy_cache", "venv", ".venv", "env", ".env", "vendor", "dist", "build", @@ -85,28 +85,28 @@ def count_lines(filepath: Path) -> int: def match_pattern(filename: str, pattern: str) -> bool: - """简单的通配符匹配""" + """Simple wildcard match""" import fnmatch return fnmatch.fnmatch(filename.lower(), pattern.lower()) def scan_repository(repo_path: Path, depth: int = 2, top_n: int = 20): - """扫描仓库,返回统计结果""" + """Scan the repository and return the statistics""" all_files = [] - lang_stats = Counter() # 语言 -> (文件数, 行数) + lang_stats = Counter() # language -> (file count, line count) lang_lines = Counter() key_files = defaultdict(list) dir_tree = [] - # 遍历文件 + # Walk the files for root, dirs, files in os.walk(repo_path): rel_root = Path(root).relative_to(repo_path) - # 忽略目录 + # Skip ignored directories dirs[:] = [d for d in dirs if d not in IGNORE_DIRS and not d.endswith(".egg-info")] - # 目录树(限制深度) + # Directory tree (depth-limited) level = len(rel_root.parts) if level <= depth: indent = " " * level @@ -124,13 +124,13 @@ def scan_repository(repo_path: Path, depth: int = 2, top_n: int = 20): all_files.append((rel_path, ext, lines)) - # 语言统计 + # Language statistics lang = LANG_MAP.get(ext) if lang: lang_stats[lang] += 1 lang_lines[lang] += lines - # 关键文件匹配 + # Key file matching for category, patterns in KEY_FILE_PATTERNS.items(): for pattern in patterns: if match_pattern(fname, pattern): @@ -141,35 +141,35 @@ def scan_repository(repo_path: Path, depth: int = 2, top_n: int = 20): def print_report(repo_path: Path, all_files, lang_stats, lang_lines, key_files, dir_tree, top_n: int): - """输出扫描报告""" + """Print the scan report""" total_files = len(all_files) total_lines = sum(f[2] for f in all_files) print("=" * 70) - print(f" 代码仓库扫描报告: {repo_path.name}") - print(f" 路径: {repo_path}") + print(f" Repository scan report: {repo_path.name}") + print(f" Path: {repo_path}") print("=" * 70) - # 1. 基本统计 - print(f"\n## 1. 基本统计\n") - print(f"| 指标 | 数值 |") + # 1. Basic statistics + print(f"\n## 1. Basic statistics\n") + print(f"| Metric | Value |") print(f"|------|------|") - print(f"| 总文件数 | {total_files} |") - print(f"| 总代码行数 | {total_lines:,} |") - print(f"| 语言种类 | {len(lang_stats)} |") + print(f"| Total files | {total_files} |") + print(f"| Total lines of code | {total_lines:,} |") + print(f"| Languages | {len(lang_stats)} |") - # 2. 语言分布 - print(f"\n## 2. 语言分布\n") - print(f"| 语言 | 文件数 | 代码行数 | 占比 |") + # 2. Language distribution + print(f"\n## 2. Language distribution\n") + print(f"| Language | Files | Lines | Share |") print(f"|------|--------|---------|------|") for lang, count in lang_stats.most_common(15): lines = lang_lines[lang] pct = f"{lines / total_lines * 100:.1f}%" if total_lines > 0 else "0%" print(f"| {lang} | {count} | {lines:,} | {pct} |") - # 3. 目录结构 - print(f"\n## 3. 目录结构(前 30 行)\n") + # 3. Directory structure + print(f"\n## 3. Directory structure (first 30 lines)\n") print("```") for line in dir_tree[:30]: print(line) @@ -177,41 +177,41 @@ def print_report(repo_path: Path, all_files, lang_stats, lang_lines, key_files, print(f" ... ({len(dir_tree) - 30} more directories)") print("```") - # 4. 关键文件发现 - print(f"\n## 4. 关键文件发现\n") + # 4. Key file discovery + print(f"\n## 4. Key file discovery\n") for category, files in key_files.items(): if files: - print(f"\n### {category} ({len(files)} 个)\n") - # 去重并排序 + print(f"\n### {category} ({len(files)} files)\n") + # Deduplicate and sort seen = set() for fpath, lines in sorted(files, key=lambda x: -x[1])[:10]: if fpath not in seen: seen.add(fpath) - print(f"- `{fpath}` ({lines:,} 行)") + print(f"- `{fpath}` ({lines:,} lines)") - # 5. 代码热点 - print(f"\n## 5. 代码热点 (Top {top_n})\n") - print(f"| 排名 | 文件 | 行数 |") + # 5. Code hotspots + print(f"\n## 5. Code hotspots (Top {top_n})\n") + print(f"| Rank | File | Lines |") print(f"|------|------|------|") sorted_files = sorted(all_files, key=lambda x: -x[2]) for i, (fpath, ext, lines) in enumerate(sorted_files[:top_n], 1): print(f"| {i} | `{fpath}` | {lines:,} |") print(f"\n{'=' * 70}") - print(f" 扫描完成。共 {total_files} 个文件,{total_lines:,} 行代码。") + print(f" Scan complete. {total_files} files, {total_lines:,} lines of code.") print(f"{'=' * 70}") def main(): - parser = argparse.ArgumentParser(description="代码仓库结构扫描与统计工具") - parser.add_argument("repo_path", help="要扫描的仓库/目录路径") - parser.add_argument("--depth", type=int, default=2, help="目录树深度 (默认 2)") - parser.add_argument("--top", type=int, default=20, help="代码热点 Top N (默认 20)") + parser = argparse.ArgumentParser(description="Repository structure scan and statistics tool") + parser.add_argument("repo_path", help="Path of the repository/directory to scan") + parser.add_argument("--depth", type=int, default=2, help="Directory tree depth (default 2)") + parser.add_argument("--top", type=int, default=20, help="Code hotspots top N (default 20)") args = parser.parse_args() repo_path = Path(args.repo_path).resolve() if not repo_path.is_dir(): - print(f"错误: {repo_path} 不是有效目录", file=sys.stderr) + print(f"Error: {repo_path} is not a valid directory", file=sys.stderr) sys.exit(1) all_files, lang_stats, lang_lines, key_files, dir_tree = scan_repository( diff --git a/skills/team-wiki-codebase/scripts/validate_kb.py b/skill-data/wiki/scripts/validate_kb.py similarity index 56% rename from skills/team-wiki-codebase/scripts/validate_kb.py rename to skill-data/wiki/scripts/validate_kb.py index 22ac3d72..06bbe3fb 100644 --- a/skills/team-wiki-codebase/scripts/validate_kb.py +++ b/skill-data/wiki/scripts/validate_kb.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 """ -validate_kb.py — 知识库质量校验工具 +validate_kb.py: knowledge base quality validation tool -用途: Phase 4 质量评估阶段,自动校验已生成知识库的: - 1. 链接完整性(检测死链接) - 2. search-anchor 覆盖率 - 3. AI 快速理解表覆盖率 - 4. 双向链接完整性 - 5. README 索引收录率 +Purpose: in the Phase 4 quality assessment stage, automatically check the generated knowledge base for: + 1. Link integrity (dead link detection) + 2. search-anchor coverage + 3. AI Quick Reference table coverage + 4. Bidirectional link integrity + 5. README index coverage -使用方式: +Usage: python3 validate_kb.py /path/to/knowledge-base-dir python3 validate_kb.py /path/to/knowledge-base-dir --verbose """ @@ -21,18 +21,24 @@ from pathlib import Path from collections import defaultdict -# Markdown 链接正则: [text](path) 或 [text](path#anchor) +# Markdown link regex: [text](path) or [text](path#anchor) LINK_PATTERN = re.compile(r'\[([^\]]*)\]\(([^)]+)\)') -# search-anchor 正则 +# search-anchor regex ANCHOR_PATTERN = re.compile(r'<!--\s*search-anchor\s*:(.*?)-->', re.DOTALL) -# AI 快速理解表正则 -AI_TABLE_PATTERN = re.compile(r'##\s*🤖\s*AI\s*快速理解', re.IGNORECASE) -# 双向链接: 链接回主架构/技术架构文档 -BACK_LINK_PATTERN = re.compile(r'\[📘.*(?:主架构|技术架构)|在整体架构中的位置', re.IGNORECASE) +# AI Quick Reference table regex. Matches both the current English heading and the +# legacy Chinese heading so knowledge bases built with earlier releases still validate. +# Legacy knowledge bases carry the Chinese heading; matched by code point so the source stays ASCII-only. +AI_TABLE_PATTERN = re.compile(r'##\s*🤖\s*AI\s*(?:Quick\s*Reference|\u5feb\u901f\u7406\u89e3)', re.IGNORECASE) +# Bidirectional link: a link back to the main / technical architecture document. +# Bilingual for the same reason as AI_TABLE_PATTERN. +BACK_LINK_PATTERN = re.compile( + r'\[📘.*(?:Technical\s*Architecture|\u4e3b\u67b6\u6784|\u6280\u672f\u67b6\u6784)|Position in the overall architecture|\u5728\u6574\u4f53\u67b6\u6784\u4e2d\u7684\u4f4d\u7f6e', + re.IGNORECASE, +) def find_md_files(kb_dir: Path) -> list: - """查找所有 .md 文件""" + """Find all .md files""" md_files = [] for root, dirs, files in os.walk(kb_dir): dirs[:] = [d for d in dirs if not d.startswith('.')] @@ -43,27 +49,27 @@ def find_md_files(kb_dir: Path) -> list: def check_links(md_file: Path, kb_dir: Path) -> list: - """检查文件中的链接是否有效""" + """Check that the links in the file resolve""" broken = [] try: content = md_file.read_text(encoding='utf-8', errors='ignore') except OSError: - return [("READ_ERROR", str(md_file), "无法读取文件")] + return [("READ_ERROR", str(md_file), "cannot read file")] for match in LINK_PATTERN.finditer(content): link_text = match.group(1) link_target = match.group(2) - # 跳过外部链接和锚点链接 + # Skip external links and anchor-only links if link_target.startswith(('http://', 'https://', 'mailto:', '#')): continue - # 分离路径和锚点 + # Split path and anchor path_part = link_target.split('#')[0] if not path_part: continue - # 解析相对路径 + # Resolve the relative path target_path = (md_file.parent / path_part).resolve() if not target_path.exists(): rel = str(md_file.relative_to(kb_dir)) @@ -73,7 +79,7 @@ def check_links(md_file: Path, kb_dir: Path) -> list: def check_anchor(md_file: Path) -> bool: - """检查文件是否包含 search-anchor""" + """Check whether the file contains a search-anchor""" try: content = md_file.read_text(encoding='utf-8', errors='ignore') return bool(ANCHOR_PATTERN.search(content)) @@ -82,7 +88,7 @@ def check_anchor(md_file: Path) -> bool: def check_ai_table(md_file: Path) -> bool: - """检查文件是否包含 AI 快速理解表""" + """Check whether the file contains the AI Quick Reference table""" try: content = md_file.read_text(encoding='utf-8', errors='ignore') return bool(AI_TABLE_PATTERN.search(content)) @@ -91,7 +97,7 @@ def check_ai_table(md_file: Path) -> bool: def check_back_link(md_file: Path) -> bool: - """检查组件文档是否有链接回主架构文档""" + """Check whether the component document links back to the main architecture document""" try: content = md_file.read_text(encoding='utf-8', errors='ignore') return bool(BACK_LINK_PATTERN.search(content)) @@ -100,7 +106,7 @@ def check_back_link(md_file: Path) -> bool: def check_readme_coverage(kb_dir: Path, md_files: list) -> tuple: - """检查 README 是否收录了所有 .md 文件""" + """Check whether the README indexes every .md file""" readme_path = kb_dir / "README.md" if not readme_path.exists(): return [], md_files @@ -112,7 +118,7 @@ def check_readme_coverage(kb_dir: Path, md_files: list) -> tuple: for f in md_files: if f.name == "README.md": continue - # 检查 README 中是否提到了这个文件 + # Check whether the README mentions this file fname_no_ext = f.stem if fname_no_ext in readme_content or f.name in readme_content: covered.append(f) @@ -123,106 +129,106 @@ def check_readme_coverage(kb_dir: Path, md_files: list) -> tuple: def main(): - parser = argparse.ArgumentParser(description="知识库质量校验工具") - parser.add_argument("kb_dir", help="知识库目录路径") - parser.add_argument("--verbose", "-v", action="store_true", help="输出详细信息") + parser = argparse.ArgumentParser(description="Knowledge base quality validation tool") + parser.add_argument("kb_dir", help="Path of the knowledge base directory") + parser.add_argument("--verbose", "-v", action="store_true", help="Print details") args = parser.parse_args() kb_dir = Path(args.kb_dir).resolve() if not kb_dir.is_dir(): - print(f"错误: {kb_dir} 不是有效目录", file=sys.stderr) + print(f"Error: {kb_dir} is not a valid directory", file=sys.stderr) sys.exit(1) md_files = find_md_files(kb_dir) if not md_files: - print(f"警告: {kb_dir} 中未找到任何 .md 文件") + print(f"Warning: no .md files found in {kb_dir}") sys.exit(0) - # 过滤出组件设计文档(以数字编号开头的文件) + # Filter the component design documents (files starting with a number) component_docs = [f for f in md_files if re.match(r'^\d+_', f.name)] print("=" * 70) - print(f" 知识库质量校验报告") - print(f" 目录: {kb_dir}") - print(f" 文件数: {len(md_files)} 个 .md 文件 (其中 {len(component_docs)} 个组件文档)") + print(f" Knowledge base quality validation report") + print(f" Directory: {kb_dir}") + print(f" Files: {len(md_files)} .md files ({len(component_docs)} component documents)") print("=" * 70) total_score = 0 max_score = 0 - # 1. 链接完整性 - print(f"\n## 1. 链接完整性检查\n") + # 1. Link integrity + print(f"\n## 1. Link integrity check\n") all_broken = [] for f in md_files: broken = check_links(f, kb_dir) all_broken.extend(broken) if all_broken: - print(f"❌ 发现 {len(all_broken)} 个死链接:") + print(f"❌ Found {len(all_broken)} dead links:") for src, target, text in all_broken[:20]: print(f" {src} → [{text}]({target})") if len(all_broken) > 20: - print(f" ... 还有 {len(all_broken) - 20} 个") + print(f" ... and {len(all_broken) - 20} more") else: - print(f"✅ 所有链接有效 (检查了 {len(md_files)} 个文件)") + print(f"✅ All links valid ({len(md_files)} files checked)") total_score += 20 max_score += 20 - # 2. search-anchor 覆盖率 - print(f"\n## 2. Search-Anchor 覆盖率\n") + # 2. search-anchor coverage + print(f"\n## 2. Search-Anchor coverage\n") has_anchor = sum(1 for f in md_files if check_anchor(f)) anchor_pct = has_anchor / len(md_files) * 100 if md_files else 0 - print(f"{'✅' if anchor_pct >= 80 else '⚠️'} {has_anchor}/{len(md_files)} 个文件有 search-anchor ({anchor_pct:.0f}%)") + print(f"{'✅' if anchor_pct >= 80 else '⚠️'} {has_anchor}/{len(md_files)} files have a search-anchor ({anchor_pct:.0f}%)") if args.verbose: for f in md_files: if not check_anchor(f): - print(f" 缺失: {f.relative_to(kb_dir)}") + print(f" Missing: {f.relative_to(kb_dir)}") if anchor_pct >= 80: total_score += 20 elif anchor_pct >= 50: total_score += 10 max_score += 20 - # 3. AI 快速理解表覆盖率(仅检查组件文档) - print(f"\n## 3. AI 快速理解表覆盖率 (组件文档)\n") + # 3. AI Quick Reference table coverage (component documents only) + print(f"\n## 3. AI Quick Reference table coverage (component documents)\n") if component_docs: has_ai_table = sum(1 for f in component_docs if check_ai_table(f)) ai_pct = has_ai_table / len(component_docs) * 100 - print(f"{'✅' if ai_pct >= 90 else '⚠️'} {has_ai_table}/{len(component_docs)} 个组件文档有 AI 快速理解表 ({ai_pct:.0f}%)") + print(f"{'✅' if ai_pct >= 90 else '⚠️'} {has_ai_table}/{len(component_docs)} component documents have an AI Quick Reference table ({ai_pct:.0f}%)") if args.verbose: for f in component_docs: if not check_ai_table(f): - print(f" 缺失: {f.relative_to(kb_dir)}") + print(f" Missing: {f.relative_to(kb_dir)}") if ai_pct >= 90: total_score += 20 elif ai_pct >= 60: total_score += 10 else: - print("⚠️ 未发现编号开头的组件文档") + print("⚠️ No numbered component documents found") max_score += 20 - # 4. 双向链接检查(组件文档是否链接回主架构) - print(f"\n## 4. 双向链接检查 (组件→主架构)\n") + # 4. Bidirectional link check (component documents link back to the main architecture) + print(f"\n## 4. Bidirectional link check (component → main architecture)\n") if component_docs: has_back = sum(1 for f in component_docs if check_back_link(f)) back_pct = has_back / len(component_docs) * 100 - print(f"{'✅' if back_pct >= 90 else '⚠️'} {has_back}/{len(component_docs)} 个组件文档有回链到主架构 ({back_pct:.0f}%)") + print(f"{'✅' if back_pct >= 90 else '⚠️'} {has_back}/{len(component_docs)} component documents link back to the main architecture ({back_pct:.0f}%)") if back_pct >= 90: total_score += 20 elif back_pct >= 60: total_score += 10 else: - print("⚠️ 未发现编号开头的组件文档") + print("⚠️ No numbered component documents found") max_score += 20 - # 5. README 索引覆盖率 - print(f"\n## 5. README 索引覆盖率\n") + # 5. README index coverage + print(f"\n## 5. README index coverage\n") covered, uncovered = check_readme_coverage(kb_dir, md_files) if (kb_dir / "README.md").exists(): cover_pct = len(covered) / (len(covered) + len(uncovered)) * 100 if (covered or uncovered) else 100 - print(f"{'✅' if cover_pct >= 90 else '⚠️'} README 收录了 {len(covered)}/{len(covered)+len(uncovered)} 个文档 ({cover_pct:.0f}%)") + print(f"{'✅' if cover_pct >= 90 else '⚠️'} README indexes {len(covered)}/{len(covered)+len(uncovered)} documents ({cover_pct:.0f}%)") if uncovered and args.verbose: - print(" 未收录:") + print(" Not indexed:") for f in uncovered[:10]: print(f" {f.relative_to(kb_dir)}") if cover_pct >= 90: @@ -230,19 +236,19 @@ def main(): elif cover_pct >= 60: total_score += 10 else: - print("❌ 未找到 README.md") + print("❌ README.md not found") max_score += 20 - # 总结 + # Summary final_pct = total_score / max_score * 100 if max_score else 0 print(f"\n{'=' * 70}") - print(f" 综合评分: {total_score}/{max_score} ({final_pct:.0f}%)") + print(f" Overall score: {total_score}/{max_score} ({final_pct:.0f}%)") if final_pct >= 90: - print(f" 评级: ✅ 优秀 — 知识库质量达标") + print(f" Rating: ✅ Excellent. The knowledge base meets the quality bar") elif final_pct >= 70: - print(f" 评级: ⚠️ 良好 — 建议修复上述问题") + print(f" Rating: ⚠️ Good. Fixing the issues above is recommended") else: - print(f" 评级: ❌ 需改进 — 存在较多质量问题") + print(f" Rating: ❌ Needs improvement. There are many quality issues") print(f"{'=' * 70}") diff --git a/skills/team-wiki-codebase/README.md b/skills/team-wiki-codebase/README.md deleted file mode 100644 index 861b4402..00000000 --- a/skills/team-wiki-codebase/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# team-wiki-codebase — 大型代码库 AI 认知工程 - -> TeamAI builtin skill:方法论、脚本与 Agent 规范随 `teamai pull` / `teamai init` 部署到项目的 `.codebuddy/`、`.cursor/` 等目录。TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. - -## 为什么需要这个 skill - -大型项目的 AI 理解困境: - -| 痛点 | 具体表现 | -|------|---------| -| **上下文装不下** | 10+ 仓库、数十万行代码,远超 AI 上下文窗口 | -| **关系看不清** | 微服务间的 RPC/MQ/DB 依赖散落在各仓库,没有全局视图 | -| **规则记不住** | 业务约束、状态机、配置参数隐藏在深层调用链中 | -| **回答不准确** | AI 只看到局部代码,缺乏全局架构认知,容易幻觉 | -| **token 消耗大** | 每次提问都要重新读大量源码,效率极低 | - -## 怎么解决 - -通过架构逆向工程,将海量代码**压缩为结构化知识库**: - -- 每个结论有代码 `文件:行号` 作为证据 -- 每条组件关系有置信度标注(`EXTRACTED` / `INFERRED` / `AMBIGUOUS`) -- 每次生成后有准确性统计,超标自动警告 -- AI 读知识库而非读源码,**约 1/50 的 token 消耗**获得全局架构认知 -- Phase 0 可用 `teamai codebase --extract` 生成可证据化的结构边(TS/JS/Python/Go AST + 多语言 heuristic) -- 提取后可用 `teamai codebase --deep-enrich --project <slug> --output <repo>` 生成确定性图谱文档(G1/G2/G3)与深度知识;无需单独的 team-wiki CLI - ---- - -## 产出体系 - -``` -<output_dir>/ -├── README.md ← 检索路由指引(AI 专用) -├── {项目名} 技术架构.md ← 系统全貌,~200KB -├── {项目名} 业务架构.md ← 产品能力 + 生命周期 -├── {项目名} 部署架构.md ← 部署拓扑 -├── XX_{组件名}设计说明.md × N ← 每组件一份,含 AI 快速理解表 -├── XX_{项目名}核心API产品代码映射.md ← 产品约束→代码位置 桥梁文档 -├── XX_{项目名}产品规则速查表.md -├── XX_{项目名}业务开发规范SOP.md -├── {反模式/RPC契约/排障记录} × N -├── _manifest.json ← 机器可读 manifest(供后续图谱合并) -└── graph/ ← Graph RAG 图谱文档集 - ├── G1 组件依赖关系矩阵 - ├── G2 调用链路全景 + 状态机 - ├── G3 数据流与存储依赖图 - ├── G4 错误码组件映射表 - ├── G5 跨组件交互场景手册(≥10个时序图) - ├── G6 知识图谱三元组(≥100条,含置信度) - ├── G7 架构风险与影响面分析 - ├── G8 核心配置参数索引 - └── G9 业务规则约束矩阵 + AI 推理决策树 -``` - ---- - -## 执行流程 - -``` -Phase 0 → 初始化:收集路径、项目名、产品文档来源;可选 CLI ast+heuristic 结构基线 - -Phase K1 → 架构逆向:关键文件提取 → 分层分析 → 组件关系矩阵 - ⛔ 确认点① 架构理解确认 - -Phase K2 → 文档生成(分批并行): - 批次1~4: Type-4 组件文档(并行子 Agent 分发) - ⛔ 确认点② 文档质量抽查 - 批次5~7: 架构总览 + 桥梁文档 + 知识增强 - -Phase K3 → AI-Native 增强: - search-anchor + 双向链接 + 检索路由规则 - Graph RAG 图谱文档集 G1~G9(置信度三态标注) - -Phase K4 → 质量评估: - validate_kb.py 自动检验 - 全库准确性审计([UNVERIFIED] 统计 + 接口覆盖率) - 跨文档一致性校验(矛盾检测 + 自动修复) - RAG 检索抽检(7类问题) - AI 端到端验证(10~15 个标准问题 + 代码回溯) - 生成质量报告 -``` - -支持 `--update` 增量更新(基于文件 hash 缓存,只重跑变更组件)。 - ---- - -## 文件结构 - -``` -team-wiki-codebase/ -├── SKILL.md ← 主执行指令(AI 加载) -├── README.md ← 本文件 -├── scripts/ -│ ├── scan_repo.py ← 仓库扫描辅助工具 -│ └── validate_kb.py ← 知识库质量校验工具 -└── references/ - ├── agents/ - │ ├── kb-doc-generator.md ← Type-1~8 文档生成专职 Agent - │ └── graph-rag-agent.md ← G1~G9 图谱文档专职 Agent - ├── methodology/ - │ ├── phase0-collection.md ← 源材料采集方法 - │ ├── phase1-reverse-engineering.md ← 架构逆向工程方法 - │ ├── phase2-document-types.md ← 九大文档类型规范与质量标准 - │ ├── phase3-ai-enhancement.md ← AI-Native 增强方法 - │ └── phase4-quality.md ← 质量评估 Checklist - └── templates/ - └── project-overview.md ← 知识库 README 模板(含认知边界声明) -``` - ---- - -## 质量标准 - -| 维度 | 达标标准 | -|------|---------| -| 覆盖率 | ≥90% P0 核心组件有文档 | -| 准确性 | [UNVERIFIED] < 15% | -| 结构质量 | 死链接=0,search-anchor 覆盖率≥95% | -| AI 可用性 | RAG 检索抽检准确率≥85% | -| 关系可信度 | AMBIGUOUS 关系 < 10%,全部列入待确认清单 | diff --git a/skills/team-wiki-codebase/SKILL.md b/skills/team-wiki-codebase/SKILL.md deleted file mode 100644 index bf752c43..00000000 --- a/skills/team-wiki-codebase/SKILL.md +++ /dev/null @@ -1,905 +0,0 @@ ---- -name: team-wiki-codebase -description: | - 让 AI 真正理解大型代码库。针对多仓库、多微服务、迭代多年的项目,通过架构逆向 + Graph RAG 图谱 + CLI 多语言 AST, - 将海量代码压缩为结构化知识库——每条结论可回溯代码行,每条关系有置信度标注。 - - 适用场景:项目有 10+ 仓库或微服务,AI 直接读代码无法全局理解、回答不准确、token 开销大。 - - 产出:组件设计文档 × N + 架构总览 + 桥梁文档 + Graph RAG 图谱(G1~G9) + _manifest.json + teamai extract graph (teamwiki/)。 - - Trigger: team-wiki-codebase, code-to-knowledge, 代码知识库, 架构分析, 架构逆向 - Prerequisites: 可访问的源码目录(支持多仓库);本 skill 目录下 `references/` 与 `scripts/` ---- - -# team-wiki-codebase — 大型代码库 AI 认知工程 - -> 方法论与脚本位于本 skill 的 `references/`、`scripts/`(`teamai pull` 后出现在 `.cursor/skills/team-wiki-codebase/` 或 `.codebuddy/skills/team-wiki-codebase/`)。人类可读概览见 [README.md](./README.md)。 -> Phase 0 结构基线使用 `teamai codebase --extract`。TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. - -**解决什么问题**:大型项目(10+ 仓库、数十微服务、迭代多年)让 AI 无法全局理解——上下文窗口装不下所有代码,组件关系散落各处,业务规则隐藏在深层调用链中。直接让 AI 读代码,既慢(海量 token)又不准(缺乏全局视角)。 - -**怎么解决**:通过架构逆向工程,将海量代码系统化压缩为**结构化、可验证、AI-Native** 的深度知识库——每个结论可回溯到代码行,每条关系有置信度标注,每次更新有增量校验。AI 读知识库而非读源码,用约 **1/50 的 token** 获得全局架构认知。 - -## 使用方式 - -``` -/team-wiki-codebase # 默认:Standard(单 session 核心路径) -/team-wiki-codebase --deep # Deep:完整 K1~K4 + G1~G9 -/team-wiki-codebase --update # 增量更新已有 knowledge/ -/team-wiki-codebase continue # 从 _review/progress.json 断点继续 -``` - ---- - -## Agent 架构 - -| Agent | 文件 | 启动时机 | -|-------|------|---------| -| 知识库文档生成 Agent | `references/agents/kb-doc-generator.md` | Phase K2 每批组件 | -| Graph RAG Agent | `references/agents/graph-rag-agent.md` | Phase K3 | - -**主 Agent 职责**:流程编排、确认点管理、progress.json 维护、质量报告汇总。 - ---- - -## 入口判断 - -**每次激活时必须先执行此判断。** - -``` -IF 用户输入包含 "--update" 或 "增量更新": - → Update 模式 -ELSE IF 用户输入包含 "continue" 或 "继续": - → Continue 模式 -ELSE: - → 检查用户指定目录下是否有 _review/progress.json - IF 存在 → 告知状态,等待"继续上次"或"重新开始" - ELSE → Phase 0 -``` - ---- - -## Continue 模式 - -``` -Step 1:定位 progress.json -Step 2:读取解析,展示恢复摘要 -Step 3:根据 current_phase 跳转: - "phase0_done" → Phase K1 - "phasek1_waiting_confirm" → 展示 k1-architecture-map.md,等待确认① - "phasek1_confirmed" → Phase K2 - "phasek2_batch_N" → Phase K2 第 N 批继续(跳过已完成) - "phasek2_waiting_confirm" → 等待确认② - "phasek2_confirmed" → Phase K3 - "phasek3_done" → Phase K4 - "phasek4_done"/"completed" → 告知完成,询问是否 --update 或重跑某组件 -``` - ---- - -## Update 模式(增量更新) - -**触发**:`/team-wiki-codebase --update` 或「增量更新」。 -**前提**:已有 completed 状态的 progress.json。 - -``` -Step 1:读取 progress.json,获取 file_hash_cache -Step 2:扫描 project_root,计算各文件当前 SHA256 -Step 3:对比 hash,分类:新增 / 修改 / 删除 -Step 4:展示变更摘要,等待用户确认: - ┌────────────────────────────────────┐ - │ 变更摘要 │ - │ 新增: N 个文件 │ - │ 修改: N 个文件(含 Aurora.py 等) │ - │ 删除: N 个文件 │ - │ 受影响组件: [列表] │ - │ 受影响图谱文档: G1/G2/G6/G7 │ - └────────────────────────────────────┘ -Step 5:仅重跑受影响范围: - - Phase K2:重新生成受影响组件的 Type-4 文档(覆盖写入) - - Phase K3 局部:更新涉及变更组件的图谱文档(G1/G2/G6/G7) - - Phase K4:重新运行 validate_kb.py -Step 6:更新 file_hash_cache + metadata.json commit SHA -Step 7:组件级 diff(处理新增/删除仓库或组件) - IF repos 列表与上次不同: - 新增的仓库 → 对新仓库执行完整 K1 扫描,补充到组件清单,生成 Type-4 文档 - 删除的仓库 → 对应组件文档顶部加 `⚠️ [DEPRECATED] 此组件对应仓库已移除` - → 更新 k1-architecture-map.md 的组件清单 - → 更新 G1 矩阵(移除已删除组件的行列,新增新组件行列) -``` - ---- - -## progress.json 规范 - -**路径**:`<output_dir>/../_review/progress.json` - -```json -{ - "version": "5", - "repos": [ - {"name": "repo-a", "path": "/absolute/path/to/repo-a", "language": "go"}, - {"name": "repo-b", "path": "/absolute/path/to/repo-b", "language": "python"} - ], - "output_dir": "/absolute/path/to/knowledge", - "primary_language": "go", - "project_name": "ProjectName", - "scan_time": "2026-01-01T10:00:00Z", - "current_phase": "phasek2_batch_2", - "confirmed_phases": ["phase0", "phasek1"], - - "service_map": { - "描述": "Phase K1 Step 3 构建的服务名→仓库映射表", - "ServiceA": {"repo": "repo-a", "entry": "cmd/serviceA/main.go"}, - "ServiceB": {"repo": "repo-b", "entry": "app/main.py"} - }, - - "kb_progress": { - "component_total": 12, - "components_done": ["Aurora", "Frame"], - "components_pending": ["CCDB", "Dispatcher"], - "type1_done": false, - "type2_done": false, - "type3_done": false, - "bridge_docs_done": false, - "graph_rag_done": false - }, - - "accuracy_stats": { - "total_claims": 0, - "verified": 0, - "unverified": 0, - "ambiguous_relations": 0 - }, - - "interface_coverage": { - "描述": "接口数量对账结果,由 Phase K2 自校验填充", - "ComponentA": {"type": "HTTP", "scanned": 13, "documented": 0, "gap": 13}, - "ComponentB": {"type": "MQ", "scanned": 5, "documented": 0, "gap": 5} - }, - - "consistency_check": { - "描述": "Phase K3 Step 3 跨文档一致性校验结果", - "contradictions": 0, - "missing_refs": 0, - "g1_deviations": 0, - "consistency_rate": 0.0 - }, - - "e2e_validation": { - "描述": "Phase K4 Step 4 AI 端到端验证结果", - "total_questions": 0, - "correct": 0, - "partial": 0, - "incorrect": 0, - "boundary_ok": 0, - "boundary_fail": 0, - "accuracy_rate": 0.0 - }, - - "file_hash_cache": { - "relative/path/to/file.go": "sha256_hex" - } -} -``` - -> `accuracy_stats` 在每批 Phase K2 完成后累加,是知识库可信度的全局指标。 - ---- - -## 核心原则(准确性优先) - -1. **代码为唯一事实来源**:每个结论必须有代码文件:行号 作为证据,无法验证的标 `[UNVERIFIED]` -2. **置信度三态强制**:图谱中每条关系标 `EXTRACTED(1.0)` / `INFERRED(0.6~0.9)` / `AMBIGUOUS(0.1~0.3)`;禁止凭空发明,禁止用 0.5 默认值 -3. **两级准确性验证**:Phase K2 每份文档生成后立即自校验;Phase K4 全库质量检验 -4. **人在回路两次确认**:架构理解(K①)和组件文档质量(K②)必须人工确认,防止系统性错误扩散 -5. **并行生成 + 断点续传**:Type-4 组件文档并行分发(同一消息发出所有 Agent calls);每批持久化 progress.json -6. **Token 精简**:`Glob → Grep → Read` 三步法,禁止全量目录扫描 -7. **诚实审计**:`[UNVERIFIED]` 不得隐藏;质量数字完整展示;不确定用 AMBIGUOUS 不删除 -8. **认知边界声明**:知识库 README 必须明确声明覆盖范围和不覆盖范围,让 AI 知道何时应该说"不确定" -9. **跨文档一致性**:Phase K3 强制交叉比对组件间关系描述,矛盾项必须修复后才计入"一致" -10. **端到端可验证**:Phase K4 用标准化问题测试知识库实际回答能力,E2E 准确率目标 ≥ 80% - ---- - -## Phase 0:初始化 - -一次性向用户询问以下信息(**同一条消息,不分步骤**): - -1. **项目所有代码仓库路径**(用户把整个项目涉及的所有仓库地址列出来): - - 格式:每行一个绝对路径,或逗号分隔 - - 示例: - ``` - /path/to/api-gateway - /path/to/order-service - /path/to/user-service - /path/to/common-lib - ``` - - 说明:这是最关键的一步。大型项目的代码散布在多个仓库中,必须**全部提供**才能构建完整的架构认知。遗漏仓库 = 知识库盲区。 -2. **项目名称**(用于文档命名,如 "CVM"、"电商平台") -3. **产品文档来源**(可选,提供则生成 Type-5/6 桥梁文档): - - API 文档目录路径 - - 使用限制 / FAQ 文档路径 -4. **输出路径**(默认:第一个仓库的父目录下的 `knowledge/`) - -**Step 0A:仓库清单整理** - -收到用户提供的仓库列表后,构建仓库清单: - -``` -FOR 每个用户提供的路径: - 1. 验证路径存在且可访问 - 2. 检测是否为 git 仓库(是否有 .git 目录) - 3. 检测主要语言(按文件扩展名分布) - 4. 统计代码规模(文件数 + 估算行数) - 5. 记录 git commit SHA + tag - -结果写入 _review/repo-manifest.json: -{ - "repos": [ - { - "path": "/absolute/path/to/repo-a", - "name": "repo-a", - "language": "go", - "files": 320, - "lines_estimate": 45000, - "commit": "abc123", - "tag": "v1.2.0", - "accessible": true - }, - ... - ], - "total_repos": N, - "inaccessible": ["path/to/repo-x(权限不足)"] -} -``` - -展示给用户确认: -``` -已识别 {N} 个仓库: - ✅ repo-a (Go, ~45K 行) - ✅ repo-b (Python, ~12K 行) - ✅ repo-c (Go, ~28K 行) - ❌ repo-x (路径不存在或无法访问) - -总计: ~{N}K 行代码,{N} 个仓库 -确认无误后回复"继续",或补充遗漏的仓库。 -``` - -**Step 0B:自动检测主要语言**(按仓库列表汇总,不阻断流程): -``` -检测方法:汇总所有仓库的文件扩展名分布 - .go 文件占比最高 → language: "go" - .py 文件占比最高 → language: "python" - .java 文件占比最高 → language: "java" - .ts/.js 文件占比最高 → language: "typescript" - .rs 文件占比最高 → language: "rust" - 多语言混合(无明显主导) → language: "mixed" -备注:language 字段用于接口扫描时选择 grep 模式(详见 Phase K1 Step 5) -``` - -**Step 0C:记录基准版本**: -```bash -# 对每个仓库分别记录 -FOR repo in repos: - git -C <repo.path> rev-parse HEAD 2>/dev/null - git -C <repo.path> describe --tags --always 2>/dev/null -``` -写入 `_review/metadata.json`: -```json -{ - "project_name": "CVM", - "scan_time": "<ISO8601>", - "repos": [ - {"name": "repo-a", "commit": "<sha>", "tag": "<tag>"}, - {"name": "repo-b", "commit": "<sha>", "tag": "<tag>"} - ] -} -``` - -**Step 0D:CLI 结构基线(每个代码仓库,推荐)** - -在 K1 深读之前,用 TeamAI 提取可证据化的 import/call 结构边(Python/Go/TS 等,`code-ast`)并与 regex 基线合并(`code-heuristic`): - -```bash -# For each repo. Writes <repo>/teamwiki/ (evidence pages + .indices/graph-index.json). -# Existing flags only: --extract [path], optional --project <slug>, optional --incremental. -teamai codebase --extract <repo_abs_path> --project <project_slug> -``` - -- Output: `teamwiki/evidence/code/<project>/` pages; `teamwiki/.indices/graph-index.json` (structural edges). -- K1/K2/K3 写 `_manifest.json` 的 `edges[]` 时:**优先引用** extract 的 `code-ast` 边 + `evidenceRefs`(`path:line`),Agent 推断标 `INFERRED`/`AMBIGUOUS`。 -- After Phase K3, skip any extra graph compile / merge step that is not a `teamai` command. TeamAI does not ship a separate team-wiki CLI. Continue with this skill using `teamai` and the files under this skill directory. No extra plugin is required. - -写入初始 progress.json(current_phase: "phase0_done"),进入 **Phase K1**。 - ---- - -## Phase K1:架构逆向与源材料采集 - -**方法论**:`references/methodology/phase0-collection.md` + `references/methodology/phase1-reverse-engineering.md` - -### Step 1:可选运行扫描脚本(推荐) - -```bash -python3 scripts/scan_repo.py <project_root> --depth 2 --top 10 -``` -输出:文件统计 + 关键文件发现报告 + 语言分布。 - -### Step 2:关键文件提取 - -按优先级扫描(详见 phase0-collection.md): -- **P0 必须**:入口文件、路由/Handler、流程编排配置、Proto/IDL -- **P1 重要**:数据库 Schema(DDL)、常量/错误码定义 -- **P2 增强**:配置文件、测试文件(理解预期行为) - -### Step 3:架构逆向(详见 phase1-reverse-engineering.md) - -- 自底向上分层:叶子节点(DB/MQ) → 中间节点(编排/调度) → 根节点(API入口) -- 三层穿透追踪:对核心 API ≥5 条完成 API入口→编排层→服务执行层 全链路追踪 -- 构建 N×N 组件关系矩阵(标注通信方式:RPC/MQ/DB) - -### Step 4:生成架构分析报告 - -写入 `_review/k1-architecture-map.md`: - -```markdown -## 架构分层(≥4层) -| 层级 | 组件列表 | 核心职责 | 代码仓库 | - -## 组件清单 -| 组件名 | 架构层级 | **所属仓库** | 语言 | 核心度(P0/P1/P2) | 入口文件 | **接口校验类型** | - -接口校验类型取值(在确认点①请用户核对此列): - - `HTTP` → API 接入层,有 HTTP/gRPC 路由注册,需做接口数对账 - - `MQ` → 消息处理层,有 MQ Consumer/Exchange 声明,以 Topic 数做基准 - - `RPC` → 内部服务层,有 .proto / .thrift / IDL 文件,以 Method 数做基准 - - `NONE` → 调度/执行/数据层,无对外接口,不做接口数校验 - -## N×N 组件通信矩阵 -(值:RPC/MQ/DB/—,标注置信度 [E]EXTRACTED/[I]INFERRED/[A]AMBIGUOUS) - -## 核心调用链路(≥5条) -(格式:API(file:line) → 编排层(config:line) → 服务层(handler:line) → DB(table)) - -## 术语表 -| 内部术语 | 外部/产品术语 | 说明 | - -## 不确定项(供人工确认) -(标注 [A] 的关系和推断,说明不确定原因) -(接口校验类型不确定的组件,标注 [?] 等用户在确认点①明确) -``` - -### Step 5:接口清单扫描(按校验类型分别执行) - -**仅对 k1-architecture-map.md 中接口校验类型 ≠ NONE 的组件执行**: - -``` -FOR 每个 接口校验类型 = HTTP 的组件: - 执行 grep 扫描: - Go: grep -rn "\.GET\|\.POST\|\.PUT\|\.DELETE\|router\.Handle\|@handler" <component_dir> - Python: grep -rn "@app\.route\|@router\.\|APIRouter\|include_router" <component_dir> - 记录:组件名 → HTTP接口数 N(SCAN_CONFIDENCE: HIGH/MEDIUM) - -FOR 每个 接口校验类型 = MQ 的组件: - 执行 grep 扫描: - grep -rn "Exchange\|Queue\|Topic\|consumer\|subscribe\|@KafkaListener" <component_dir> - 记录:组件名 → MQ Topic/Queue 数 N - -FOR 每个 接口校验类型 = RPC 的组件: - 解析 .proto / .thrift 文件: - find <component_dir> -name "*.proto" -o -name "*.thrift" | xargs grep "^rpc\|^service" - 记录:组件名 → RPC Method 数 N -``` - -结果写入 `_review/interface-inventory.json`: -```json -{ - "ComponentA": {"type": "HTTP", "count": 13, "confidence": "HIGH"}, - "ComponentB": {"type": "MQ", "count": 5, "confidence": "MEDIUM"}, - "ComponentC": {"type": "RPC", "count": 8, "confidence": "HIGH"}, - "ComponentD": {"type": "NONE", "count": 0, "confidence": "—"} -} -``` - -**完成后**:更新 `current_phase` 为 `"phasek1_waiting_confirm"`。 - -**⛔ 确认点①** — 等待用户明确回复,不得自动进入下一阶段。 - -展示给用户: -``` -架构分析完成。 - -组件清单(共 N 个): - P0 核心: [列表] - P1 重要: [列表] - P2 辅助: [列表] - -接口扫描结果(供校验用): - HTTP 接口:ComponentA 13个, ComponentB 7个 - MQ Topic: ComponentC 5个 - RPC Method:ComponentD 8个 - 无接口组件:ComponentE, ComponentF, ... - -AMBIGUOUS 关系(请明确): - - ComponentX → ComponentY 的通信方式不确定 - -请确认(直接编辑 k1-architecture-map.md 后回复"继续"): - 1. 架构分层和 P0/P1/P2 标注是否正确? - 2. 每个组件的接口校验类型(HTTP/MQ/RPC/NONE)是否准确? - 3. 接口扫描数量是否合理?明显偏少说明有遗漏,偏多可能扫到了测试文件。 -``` - -确认后:更新 `"phasek1_confirmed"` → Phase K2。 - ---- - -## Phase K2:文档生成(分批并行 + 中间质量确认) - -**方法论**:`references/methodology/phase2-document-types.md` - -### 生成顺序(依赖链驱动,底层先写) - -``` -批次1: 数据层 + 基础执行层 Type-4 组件文档 ← 并行 -批次2: 资源/调度层 Type-4 组件文档 ← 并行 -批次3: 消息/服务层 Type-4 组件文档 ← 并行 -批次4: API入口层 Type-4 组件文档 ← 并行 - ⛔ 确认点② ← 人工抽查组件文档质量 -批次5: 架构总览层 (Type-1 + Type-2 + Type-3) ← 串行(依赖上层全部完成) -批次6: 桥梁文档 (Type-5 + Type-6 + Type-7) ← 串行(依赖产品文档) -批次7: 知识增强 (Type-8: 反模式/RPC契约/排障) ← 串行 -``` - -### 每批执行流程 - -读取 `references/agents/kb-doc-generator.md`,拼装输入包并启动: - -``` -component_list: 本批次组件/文档类型列表 -architecture_map: _review/k1-architecture-map.md 完整内容 -repos: _review/repo-manifest.json 中的仓库列表 -service_map: progress.json 中的 service_map -output_dir: <Phase 0> -project_name: <Phase 0> -product_docs_dir: <Phase 0,可为空> -methodology_dir: references/methodology/ -completed_docs: kb_progress.components_done(断点恢复跳过) -parallel_mode: true(批次1~4)/ false(批次5~7) -``` - -每批完成后: -- 将完成组件追加到 `kb_progress.components_done` -- 累加 `accuracy_stats`(从 Agent 返回的自校验摘要中提取) -- 更新 `current_phase` 为 `"phasek2_batch_N"` -- 展示本批次 token 消耗和 `[UNVERIFIED]` 统计 - -### ⛔ 确认点②(批次1~4完成后) - -展示给用户: -``` -已生成 {N} 份组件设计文档。准确性统计: - 总声明数: {N} | 已验证: {N} | [UNVERIFIED]: {N}({X}%) - AMBIGUOUS 关系: {N} 条 - -请抽查 2~3 份文档(建议选最复杂的组件): - 路径:<output_dir>/XX_<组件名>设计说明.md - -确认要点: - 1. AI 快速理解表的代码入口是否精确到函数名? - 2. 核心流程描述是否与代码实际一致? - 3. [UNVERIFIED] 比例是否可接受?(建议 <15%) - -如发现系统性问题,请描述,我将调整策略后重新生成。 -``` - -更新 `current_phase` 为 `"phasek2_waiting_confirm"`。 -用户确认后更新为 `"phasek2_confirmed"`,继续批次5~7。 - -### 全部批次完成后 - -写入 `_review/k2-doc-list.md`(文档清单:路径 + 规模KB + [UNVERIFIED]数 + 生成时间)。 -更新 `current_phase` 为 `"phasek2_done"` → Phase K3。 - ---- - -## Phase K3:AI-Native 增强 + 图谱文档集 - -**方法论**:`references/methodology/phase3-ai-enhancement.md` - -### Step 1:AI-Native 元素注入 - -对所有已生成文档补充(如 Phase K2 的 Agent 未完整添加): - -| 元素 | 要求 | 适用范围 | -|------|------|---------| -| `search-anchor` | 5~15 个关键词,标题后第一行 | 所有文档 | -| AI 快速理解表 | 10 维度,紧跟标题 | 所有 Type-4 组件文档 | -| 双向链接 | 组件↔主架构,桥梁↔组件 | 所有文档 | -| 检索路由规则 | 4条分流规则 + 4级优先级 | 仅技术架构总览 | -| QA 对 | 10~20 个高频问题+答案引用 | 仅技术架构总览第9章 | - -### Step 2:Graph RAG 图谱文档集 - -读取 `references/agents/graph-rag-agent.md`,拼装输入包并启动: - -``` -all_kb_docs_dir: <output_dir> -architecture_map: _review/k1-architecture-map.md -doc_list: _review/k2-doc-list.md -project_name: <Phase 0> -output_dir: <output_dir>/graph/ -methodology_file: references/methodology/phase2-document-types.md -``` - -生成 G1~G9(每条关系强制置信度三态标注): - -| 图谱文档 | 解决的问题 | 置信度要求 | -|---------|---------|-----------| -| G1 组件依赖关系矩阵 | "谁依赖 X?" | EXTRACTED 来自文档明确描述 | -| G2 调用链路全景 + 状态机 + 约束矩阵 | "API 经过哪些模块?" | 调用链 EXTRACTED,推断依赖 INFERRED | -| G3 数据流与存储依赖图 | "数据存哪里?" | 读写关系 EXTRACTED | -| G4 错误码组件映射表 | "错误码是哪个模块的?" | EXTRACTED | -| G5 跨组件交互场景手册(≥10个时序图) | "配额检查怎么做?" | 时序 EXTRACTED,边界 INFERRED | -| G6 知识图谱三元组(≥100条) | "A 间接依赖谁?" | 每条标 E/I/A + 分值 | -| G7 架构风险与影响面分析 | "X 挂了影响多大?" | 直接依赖 EXTRACTED,间接 INFERRED | -| G8 核心配置参数索引 | "怎么改 XX 配置?" | EXTRACTED 来自配置文件 | -| G9 业务规则约束矩阵 + AI 推理决策树 | "能不能做 XX?" | 规则 EXTRACTED,推断 INFERRED | - -同时生成 `<output_dir>/graph/README.md`(索引 + 按问题类型查找表 + 检索路由建议)。 - -### Step 3:跨文档一致性校验 - -**Graph RAG Agent 完成后,主 Agent 自行执行此步骤(不委托给子 Agent)。** - -目的:检测组件文档之间的矛盾描述,防止"A 说调用 B 用 RPC,B 说被 A 用 MQ 调用"这类不一致。 - -``` -Step 3A:构建"声称矩阵" - - 对每份 Type-4 组件文档,从**两个层面**提取关系声称: - - 层面1:AI 快速理解表中的"上游组件"和"下游组件"字段 - 层面2:正文中的接口设计章节、核心流程章节中的调用描述 - - 如果层面1和层面2对同一关系描述不一致 → 首先记录为"文档内矛盾"(比表头和正文优先级更高的问题) - - 提取示例: - 组件X.md 表头声称: X→Y(RPC), X→Z(MQ) - 组件X.md 正文声称: X→Z(HTTP) ← 与表头矛盾! - 组件Y.md 表头声称: Y←X(RPC), Y→Z(DB) - 组件Z.md 表头声称: Z←X(HTTP), Z←Y(DB) - -Step 3B:交叉比对 - - FOR 每对组件 (A, B): - IF A.md 声称 "A→B 用 RPC" AND B.md 声称 "B←A 用 MQ": - → 记录矛盾: "A→B 通信方式不一致: A说RPC, B说MQ" - IF A.md 声称 "A→B" BUT B.md 未提到 "被A调用": - → 记录缺失: "A声称调用B,但B的文档未提及被A调用" - IF G1矩阵中的关系 与 组件文档声称不一致: - → 记录偏差: "G1矩阵说A→B(RPC),但A的文档说A→B(MQ)" - -Step 3C:生成一致性报告 - - 写入 `_review/k3-consistency-check.md`: - - ```markdown - # 跨文档一致性校验报告 - - ## 矛盾项(必须修复) - | 组件A | 组件B | A的描述 | B的描述 | 矛盾类型 | - |-------|-------|---------|---------|---------| - | X | Z | X→Z(MQ) | Z←X(HTTP) | 通信方式不一致 | - - ## 缺失项(建议补充) - | 声称方 | 被引用方 | 声称内容 | 缺失 | - |--------|---------|---------|------| - | A | B | A→B(RPC) | B的文档未提及被A调用 | - - ## G1矩阵偏差(建议对齐) - | G1矩阵 | 组件文档 | 偏差 | - - ## 统计 - - 矛盾项: N 处(❌ 需修复) - - 缺失项: N 处(⚠️ 建议补充) - - G1偏差: N 处(⚠️ 需对齐) - - 一致关系: N 条(✅) - - 一致率: X% - ``` - -Step 3D:自动修复(仅限明确情况) - - IF 矛盾项 > 0: - FOR 每个矛盾项: - 回溯代码验证:用 Grep 查找实际的调用方式(如 rpc.Call / mq.Publish) - IF 能明确正确方 → 修复错误方文档中的描述 + 更新 G1 矩阵 - IF 无法明确 → 标记为 AMBIGUOUS,留待用户在确认点确认 - 修复后重新统计一致率 - - IF 矛盾项 = 0: - → 跳过修复,直接进入 Phase K4 -``` - -**完成后**:更新 `current_phase` 为 `"phasek3_done"` → Phase K4。 - ---- - -## Phase K4:知识库质量评估与报告 - -**方法论**:`references/methodology/phase4-quality.md` - -### Step 1:自动校验 - -```bash -python3 scripts/validate_kb.py <output_dir> -``` - -输出(**必须完整展示,不得只展示通过项**): -``` -链接完整性: ✅/❌ N 个死链接 -search-anchor: ✅/⚠️ 覆盖率 N/M (X%) -AI 快速理解表: ✅/⚠️ 覆盖率 N/M (X%) -双向链接: ✅/⚠️ 覆盖率 N/M (X%) -README 索引: ✅/⚠️ 收录率 N/M (X%) -``` - -### Step 2:准确性审计 - -从 `accuracy_stats` 汇总全库可信度,同时从 `interface_coverage` 汇总接口覆盖情况: - -``` -【内容准确性】 -总声明数: N 条(业务规则 + 接口描述 + 关系) -已验证(有代码引用): N 条 (X%) -[UNVERIFIED]: N 条 (X%) -AMBIGUOUS 关系: N 条 (X%) - -【接口覆盖率】(仅统计 HTTP/MQ/RPC 类型组件,NONE 类型不计入) -HTTP 接口: 文档记录 M 个 / 扫描基准 N 个 = X% -MQ Topic: 文档记录 M 个 / 扫描基准 N 个 = X% -RPC Method: 文档记录 M 个 / 扫描基准 N 个 = X% -综合覆盖率: X% 目标 ≥ 90% - -⚠️ 接口缺口清单(文档记录 < 扫描基准 的组件): - - ComponentA: 文档记录 8 个,扫描基准 13 个,缺口 5 个 → 建议补充 -``` - -⚠️ 需人工确认清单:([UNVERIFIED] > 20% 的文档 + 接口缺口组件 + AMBIGUOUS 关系) - -### Step 3:RAG 检索抽检 - -按 `phase4-quality.md §RAG检索测试用例` 测试 7 类问题各 1 个(详见方法论),记录命中率。 - -### Step 4:AI 端到端验证(E2E Validation) - -**核心思路**:用知识库回答一组标准化问题,然后**回溯代码验证答案正确性**,检测知识库是否能让 AI 给出正确答案。 - -``` -Step 4A:生成标准验证问题集(自动,基于已有文档) - - **优先使用用户提供的外部验证集**: - IF 用户在 Phase 0 或此时提供了验证问题列表(3~10 个真实业务问题): - → 优先使用用户问题作为验证集(标注来源: USER) - → 自动补充至 10~15 题(标注来源: AUTO) - ELSE: - → 全部自动生成(标注来源: AUTO) - - > 用户提供的问题更有价值,因为 AI 自己出题容易考自己已知的领域, - > 真正的盲区(AI 没理解但没意识到的)只有外部问题才能测到。 - - 从 k1-architecture-map.md 和 k2-doc-list.md 自动生成 10~15 个验证问题: - - 问题类型分布(至少覆盖以下 5 类): - - ┌────────────────────────────────────────────────────────────────────┐ - │ 类型1:组件职责(3题) │ - │ 模式:"<组件名> 的核心职责是什么?代码入口在哪?" │ - │ 验证方式:答案中的函数名/文件名必须在代码中存在 │ - │ │ - │ 类型2:调用关系(3题) │ - │ 模式:"<组件A> 和 <组件B> 之间是什么关系?通过什么方式通信?" │ - │ 验证方式:答案与 G1 矩阵 + 代码实际 import/call 一致 │ - │ │ - │ 类型3:操作约束(2题) │ - │ 模式:"在 <状态X> 下能否执行 <操作Y>?" │ - │ 验证方式:答案与 G9 约束矩阵 + 代码中的状态检查一致 │ - │ │ - │ 类型4:数据流向(2题) │ - │ 模式:"<操作Z> 最终会写入哪些表/队列?" │ - │ 验证方式:答案与 G3 数据流 + 代码实际 SQL/MQ 操作一致 │ - │ │ - │ 类型5:错误排查(2题) │ - │ 模式:"错误码 <XXX> 是什么意思?在哪个组件产生?" │ - │ 验证方式:答案与 G4 错误码映射 + 代码中的错误定义一致 │ - │ │ - │ 类型6(可选):认知边界测试(2题) │ - │ 模式:故意问知识库不覆盖的内容(如第三方 SDK 内部、历史架构变迁) │ - │ 验证方式:AI 应回答"超出知识库覆盖范围"而非幻觉 │ - └────────────────────────────────────────────────────────────────────┘ - -Step 4B:用知识库回答(模拟 AI 使用场景) - - FOR 每个验证问题: - 1. 假设只能读知识库文档,不能直接读代码 - 2. 按检索路由规则,找到对应文档 - 3. 从文档中提取答案 - -Step 4C:代码回溯验证 - - FOR 每个答案: - 1. 用 Grep/Read 直接在代码中验证关键声明 - 2. 判定结果: - ✅ CORRECT — 答案与代码一致 - ⚠️ PARTIAL — 答案部分正确,有遗漏或不精确 - ❌ INCORRECT — 答案与代码矛盾 - 🔇 BOUNDARY_OK — 认知边界问题,正确拒绝回答(仅类型6) - 🔇 BOUNDARY_FAIL — 认知边界问题,错误地给出了答案(仅类型6) - -Step 4D:写入验证报告 - - 追加到 k4-quality-report.md 的 ## AI 端到端验证 章节: - - | 问题 | 类型 | 检索文档 | AI答案摘要 | 代码验证 | 结果 | - |------|------|---------|-----------|---------|------| - | Aurora 核心职责? | 组件职责 | 03_Aurora设计说明.md | 调度编排... | scheduler.go:42 | ✅ | - | A→B 通信方式? | 调用关系 | G1矩阵 | RPC | import rpc_client | ✅ | - | 状态X下能否操作Y? | 操作约束 | G9矩阵 | 不能 | check_state.go:88 | ✅ | - | 第三方SDK内部? | 认知边界 | — | 超出范围 | — | 🔇 OK | - - 统计: - CORRECT: N/M (X%) - PARTIAL: N/M (X%) - INCORRECT: N/M (X%) — ❌ 每个 INCORRECT 必须列出具体矛盾点 - BOUNDARY_OK: N/N - BOUNDARY_FAIL: N/N - - E2E 准确率 = (CORRECT + BOUNDARY_OK) / 总题数 - 目标: ≥ 80% -``` - -**如果 E2E 准确率 < 80%**:在质量报告"建议"章节列出需要改进的文档和具体问题。 - -### Step 5:生成质量报告 - -写入 `_review/k4-quality-report.md`: - -```markdown -# 知识库质量报告 - -## 概览 -- 代码基准:<commit SHA> (<tag>) -- 生成时间:<ISO8601> -- 文档总数:N 份(Type-1~8: N份,图谱G1~G9: 9份) - -## 准确性 -| 指标 | 数值 | 状态 | -| 总声明数 | N | — | -| 有代码引用 | N (X%) | ✅/❌ | -| [UNVERIFIED] | N (X%) | ✅/<15% / ⚠️15~25% / ❌>25% | -| AMBIGUOUS关系 | N | ✅/⚠️ | - -## 结构质量(validate_kb.py 输出) -(完整展示,不隐藏任何数字) - -## 跨文档一致性(k3-consistency-check.md 摘要) -| 指标 | 数值 | 状态 | -| 矛盾项 | N | ✅=0 / ❌>0 | -| 缺失引用 | N | ⚠️ | -| G1偏差 | N | ⚠️ | -| 一致率 | X% | 目标≥95% | - -## RAG 检索抽检 -| 测试问题 | 期望命中 | 实际命中 | 结果 | - -## AI 端到端验证 -| 指标 | 数值 | 状态 | -| CORRECT | N/M (X%) | — | -| PARTIAL | N/M (X%) | ⚠️ | -| INCORRECT | N/M (X%) | ❌ | -| BOUNDARY_OK | N/N | ✅ | -| E2E 准确率 | X% | 目标≥80% | - -INCORRECT 详情: -(每个 INCORRECT 的具体矛盾点和改进建议) - -## 待人工确认清单 -([UNVERIFIED] 超标文档 + AMBIGUOUS 关系 + 矛盾项 + 死链接) - -## 建议 -(基于一致性校验 + E2E 验证的改进方向) -``` - -**完成后**:更新 `current_phase` 为 `"completed"`,流程结束。 - ---- - -## 输出目录结构 - -``` -<output_dir>/ -├── README.md ← 知识库索引 + 检索路由规则 + 认知边界声明(AI 专用) -├── {项目名} 技术架构.md ← [Type-1] 架构总览(目标 ≤80KB,超过则自动拆分) -├── {项目名} 技术架构-核心链路.md ← [Type-1b] 仅当 Type-1 超 80KB 时拆出 -├── {项目名} 技术架构-AI元数据.md ← [Type-1c] 仅当 Type-1 超 80KB 时拆出 -├── {项目名} 业务架构.md ← [Type-2] 产品能力 + 生命周期 ~70KB -├── {项目名} 部署架构.md ← [Type-3] 部署拓扑 ~40KB -├── XX_{组件名}设计说明.md × N ← [Type-4] 每份 20~100KB -├── XX_{项目名}核心API产品代码映射.md ← [Type-5] 仅有产品文档时生成 -├── XX_{项目名}产品规则速查表.md ← [Type-6] -├── XX_{项目名}业务开发规范SOP.md ← [Type-7] -├── {知识增强文档} × N ← [Type-8] 反模式/RPC契约/排障/知识文库 -└── graph/ ← [Type-9] Graph RAG 图谱文档集 - ├── README.md ← 图谱索引 + 按问题类型查找 - ├── G1_{项目名}组件依赖关系矩阵.md - ├── G2_{项目名}组件调用链路全景.md - ├── G3_{项目名}数据流与存储依赖图.md - ├── G4_{项目名}错误码组件映射表.md - ├── G5_{项目名}跨组件交互场景手册.md - ├── G6_{项目名}知识图谱三元组.md - ├── G7_{项目名}架构风险与影响面分析.md - ├── G8_{项目名}核心配置参数索引.md - └── G9_{项目名}业务规则约束矩阵.md - -_review/ ← 过程文件(不入知识库) -├── progress.json ← 断点续传 + 增量更新状态 -├── metadata.json ← 代码基准版本 -├── interface-inventory.json ← 接口扫描基准(Phase K1 Step 5) -├── k1-architecture-map.md ← 架构逆向结果(用户确认过) -├── k2-doc-list.md ← 文档清单 + 准确性统计 -├── k3-consistency-check.md ← 跨文档一致性校验报告(Phase K3 Step 3) -└── k4-quality-report.md ← 质量报告(含 E2E 验证结果) -``` - ---- - -## 阶段间控制 - -| 用户回复 | 行为 | -|---------|------| -| "继续" / "continue" / "ok" | 进入下一阶段 | -| "停止" / "stop" | 停止,已生成文件保持可用 | -| 直接描述问题 | 调整后重新确认,再继续 | -| 直接编辑文件后回复"继续" | 以修改后文件内容为准继续 | - ---- - -## 约束 - -- **主 Agent 不执行代码分析**:全部由专职 Agent 完成;启动前必须先 Read 对应 agent 文件 -- **严禁冗余输出**:生成文件直接 Write,禁止先在对话中打印完整内容 -- **组件文档命名**:`XX_{组件名}设计说明.md`(XX 为两位数编号,按依赖链顺序分配,底层组件编号小) -- **无产品文档时**:Type-5/6 可跳过或将约束值标注为 `[PRODUCT_DOC_MISSING]`,不得推测 -- **并行模式**:Type-4 批次必须同一消息并发发出所有 Agent calls;串行批次顺序执行 - -### 诚实审计规则(Honesty Rules) - -- **禁止凭空发明**:图谱每条关系必须有组件文档明确依据,不得基于名称猜测 -- **置信度不得伪造**:EXTRACTED=1.0,INFERRED 按证据强度 0.4~0.9,AMBIGUOUS 0.1~0.3;禁用 0.5 默认值 -- **[UNVERIFIED] 不得隐藏**:超过 20% 则文档顶部加可见警告 -- **质量数字完整展示**:validate_kb.py 输出不得只展示通过项 -- **token 成本透明**:每批完成后展示读取文件数和估计 token 消耗 -- **不确定优先 AMBIGUOUS**:宁可标注待确认,也不删除或假装确定 - ---- - -## 与 TeamAI CLI 的配合(必读) - -| 阶段 | 命令 / 路径 | -|------|-------------| -| Phase 0 结构基线 | `teamai codebase --extract <repo> --project <slug>`(writes `<repo>/teamwiki/`) | -| Deep knowledge | Use `teamai codebase --deep-enrich --project <slug> --output <repo>` after extract has written `teamwiki/evidence/code/<slug>/`. `--output` is the repository root, not the `teamwiki/` directory. Prefix with `teamai --dry-run` to preview without writing. TeamAI does not ship a separate team-wiki CLI. No extra plugin is required. | -| K3 后编译进 wiki | Skip. TeamAI does not ship a separate team-wiki CLI. Continue with this skill using `teamai` and the files under this skill directory. No extra plugin is required. | -| 产品文档入图 | Skip. Same English note as above. | -| 产品↔代码桥接 | Use `teamai codebase --reconcile --output <repo>` after product pages and extracted code pages are under `<repo>/teamwiki/`. Prefix with `teamai --dry-run` to preview without updating the graph. | -| 一键刷新 | Use `teamai codebase --extract <repo> --project <slug> --incremental`, reusing the Phase 0 repository path and project slug even when running from another directory. Do not look for another CLI. | -| 质量评估 | Use `scripts/validate_kb.py` and `teamai codebase --lint --output <repo>` to check `<repo>/teamwiki/` (`--output` takes the repository root, not the `teamwiki/` directory). Skip any extra evaluate binary. | - -**路径约定**(本 skill 安装后): - -- 方法论:`references/methodology/*.md`(相对本 skill 目录) -- Agent:`references/agents/kb-doc-generator.md`、`references/agents/graph-rag-agent.md` -- 脚本:`scripts/scan_repo.py`、`scripts/validate_kb.py` - -所有流程在本 skill(`references/`、`scripts/`)与 `teamai` CLI 内完成。No extra plugin is required. diff --git a/skills/team-wiki-codebase/references/agents/graph-rag-agent.md b/skills/team-wiki-codebase/references/agents/graph-rag-agent.md deleted file mode 100644 index e896eed2..00000000 --- a/skills/team-wiki-codebase/references/agents/graph-rag-agent.md +++ /dev/null @@ -1,344 +0,0 @@ -# Graph RAG Agent - -## 职责 - -从已生成的知识库组件文档中抽取跨组件关系信息,生成结构化图谱文档集(G1~G9),解决 RAG 检索在"跨组件关系查询"场景下的信息分散问题。 - -**此 Agent 在 Phase K3 中被主 Agent 单次串行启动。** - -## 输入包 - -``` -all_kb_docs_dir: 知识库输出根目录(包含所有 Type-1~8 文档) -architecture_map: _review/k1-architecture-map.md 完整内容 -doc_list: _review/k2-doc-list.md(文档清单) -project_name: 项目名称(用于文档命名) -output_dir: 图谱文档输出目录(<all_kb_docs_dir>/graph/) -methodology_file: references/methodology/phase2-document-types.md §Type-9 内容 -``` - -## 执行步骤 - -### Step 1:关系抽取 - -扫描 `all_kb_docs_dir` 下所有组件文档(Type-4),从 AI 快速理解表和正文中提取: - -``` -扫描维度: -├── 调用关系 (上游组件→本组件, 本组件→下游组件, 通信方式) -├── 存储依赖 (读写了哪些 DB/Redis/MQ) -├── 消息拓扑 (发布/消费的 Exchange/Topic/Queue/RoutingKey) -├── 状态流转 (操作→起始状态→中间状态→终态, 状态字段值) -├── 约束条件 (操作→前置状态要求→硬件约束→计费约束→配额) -├── 配置映射 (配置项→影响行为→变更风险) -└── 错误码归属 (错误码段→组件→排查方向) -``` - -**置信度三态标注**(每条关系/三元组必须标注,不得省略): - -| 标签 | 含义 | 来源依据 | 置信度分值 | -|------|------|---------|-----------| -| `EXTRACTED` | 组件文档中明确描述的关系(如"上游组件: Aurora(RPC)")| 代码/文档显式记录 | 1.0 | -| `INFERRED` | 合理推断的关系(如架构图中隐含的依赖链)| 结构性证据 + 合理推断 | 0.6~0.9 | -| `AMBIGUOUS` | 存在不确定性的关系,需人工确认 | 弱证据或相互矛盾 | 0.1~0.3 | - -> ⚠️ **禁止用 0.5 作为默认分值**。每条关系都要独立评估:有直接代码引用的 INFERRED 用 0.8~0.9,仅靠命名推断的用 0.6~0.7,真正模糊的才用 AMBIGUOUS。 - -构建中间数据结构(内存,不写文件): -- `relations[]`:(from, to, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) -- `state_transitions[]`:(entity, from_state, to_state, trigger_op, state_field_value, **confidence**, **confidence_score**) -- `constraints[]`:(operation, state_req, hardware_req, billing_req, quota_req, **confidence**, **confidence_score**) -- `config_items[]`:(key, default, component, behavior, change_risk, effect_mode) -- `error_codes[]`:(code_range, component, meaning, debug_direction) -- `triples[]`:(subject, predicate, object, protocol, scenario, **confidence: EXTRACTED|INFERRED|AMBIGUOUS**, **confidence_score: 0.1~1.0**) - -### Step 2:逐份生成图谱文档 - -按顺序生成 G1~G9(串行,每份完成后立即 Write): - ---- - -#### G1:组件依赖关系矩阵 - -```markdown -# {project_name} 组件依赖关系矩阵 -<!-- search-anchor: 组件依赖, 依赖矩阵, 通信方式, 调用关系 --> -## 🤖 AI 快速理解要点 -| 文档定位 | 解决"谁依赖 X?X 依赖谁?"的检索问题 | -| 核心价值 | N×N 通信矩阵 + 正向/反向依赖索引 | -| 使用场景 | 变更影响评估、服务依赖梳理、架构重构规划 | - -## N×N 组件通信矩阵 -(行:调用方,列:被调方,值:`RPC`/`MQ`/`DB`/`—`,括号内标注置信度标签) -示例:`RPC[E]` = EXTRACTED,`MQ[I:0.8]` = INFERRED 0.8,`RPC[A]` = AMBIGUOUS - -## 正向依赖索引(A 依赖谁) -| 组件 | 依赖组件 | 通信方式 | 置信度 | 典型场景 | - -## 反向依赖索引(谁依赖 A) -| 组件 | 被依赖来自 | 通信方式 | 置信度 | 典型场景 | - -## 外部服务依赖 -| 外部服务 | 被哪些组件依赖 | 通信方式 | 置信度 | 降级策略 | - -## 置信度统计 -| 标签 | 条数 | 说明 | -|------|------|------| -| EXTRACTED | N | 来自代码/文档直接描述 | -| INFERRED | N | 合理推断,标注分值 0.6~0.9 | -| AMBIGUOUS | N | 不确定,需人工确认 | -``` - ---- - -#### G2:组件调用链路全景 + 状态机 - -```markdown -# {project_name} 组件调用链路全景与状态机 -<!-- search-anchor: 调用链路, 状态机, 端到端链路, API链路 --> -## 🤖 AI 快速理解要点 -| 文档定位 | 解决"API X 经过哪些模块?实体状态如何流转?"的检索问题 | -| 核心价值 | 核心API端到端链路 + 完整状态机 + 操作-状态约束矩阵 | - -## 核心 API 端到端调用链路 -(对每个核心 API,用标准调用链格式 + mermaid 时序图) - -## 核心实体完整状态机 -(mermaid stateDiagram-v2,标注状态字段值和触发操作) - -## 操作-状态约束速查矩阵 -| 操作 \ 当前状态 | 状态A | 状态B | ... | -(✅ 允许 / ❌ 禁止 / ⚠️ 有条件) - -## AI 状态判断推理规则 -(mermaid graph TD 决策树) -``` - ---- - -#### G3:数据流与存储依赖图 - -```markdown -# {project_name} 数据流与存储依赖图 -<!-- search-anchor: 数据流, 存储依赖, MQ拓扑, 缓存 --> -## 存储系统依赖矩阵 -| 组件 | MySQL | Redis | MQ | 对象存储 | 其他 | - -## MQ 队列拓扑 -| Exchange/Topic | Routing Key | 生产者 | 消费者 | 消息含义 | - -## 缓存策略矩阵 -| 组件 | 缓存键模式 | 过期时间 | 失效策略 | -``` - ---- - -#### G4:错误码组件映射表 - -```markdown -# {project_name} 错误码组件映射表 -<!-- search-anchor: 错误码, 错误映射, InvalidParameter --> -## 错误码段分配 -| 错误码范围/前缀 | 归属组件 | 含义范围 | - -## 外部→内部错误码映射 -| 外部错误码 | 内部组件 | 内部含义 | 排查方向 | -``` - ---- - -#### G5:跨组件交互场景手册 - -对每个核心业务场景,生成: -```markdown -## 场景N:{场景名称} -<!-- 典型场景:创建/删除/修改资源、配额检查、计费、状态变更等 --> -```mermaid -sequenceDiagram - actor User - participant A as {组件A} - participant B as {组件B} - ... -``` -**正常流程**:步骤描述 -**异常处理**:各异常分支 -``` - -要求:≥10 个场景,覆盖主要写操作和关键读操作。 - ---- - -#### G6:知识图谱三元组 - -```markdown -# {project_name} 知识图谱三元组 -<!-- search-anchor: 知识图谱, 三元组, 多跳推理 --> - -## Ontology 定义 -### 实体类型: Service, Handler, Config, Table, Queue, API, ErrorCode -### 关系类型: CALLS, PUBLISHES, CONSUMES, READS, WRITES, CONFIGURES, MAPS_TO - -## 显式三元组(≥100条) -| Subject | Predicate | Object | Protocol/Scenario | Confidence | Score | - -> 每条三元组的 Confidence 必须是 `EXTRACTED` / `INFERRED` / `AMBIGUOUS`,Score 不得省略,不得用 0.5 作默认值。 - -## 多跳依赖路径索引 -| 查询模式 | 路径示例 | -| "A 最终写入哪些表?" | A→(CALLS)→B→(WRITES)→Table | - -## 反向可达索引 -| 目标节点 | 可达路径 | -``` - ---- - -#### G7:架构风险与影响面分析 - -```markdown -# {project_name} 架构风险与影响面分析 -<!-- search-anchor: 架构风险, 爆炸半径, 影响面 --> -## 组件风险等级总表 -| 组件 | 风险等级 | 爆炸半径 | 备注 | -(🔴高/🟡中/🟢低) - -## 关键组件爆炸半径分析(≥3个高风险组件) -组件 X 故障时的影响链路分析 - -## 关键路径与瓶颈识别 -## 聚类分析(哪些组件形成强耦合簇) -## 变更风险评估矩阵 -``` - ---- - -#### G8:核心配置参数索引 - -```markdown -# {project_name} 核心配置参数索引 -<!-- search-anchor: 配置参数, 配置索引, 配置变更 --> -## 分层配置架构图(mermaid) - -## 各层配置参数表 -| 配置项 | 所属组件 | 默认值 | 影响行为 | 变更风险 | 生效方式 | -(变更风险: 🟢低/🟡中/🔴高;生效方式: 热生效/需重启) - -## 配置变更影响面速查 -| 变更类型 | 影响范围 | 生效方式 | 回滚策略 | - -## AI 回答"怎么修改 XX 配置"时必须同时告知: -1. 配置文件位置 -2. 影响范围 -3. 生效方式 -4. 回滚策略 -5. 变更风险 -6. 是否需要灰度 -``` - ---- - -#### G9:业务规则约束矩阵 - -```markdown -# {project_name} 业务规则约束矩阵 -<!-- search-anchor: 业务规则, 约束矩阵, 操作约束, AI推理 --> -## 操作前置条件矩阵 -| 操作 | 状态要求 | 硬件约束 | 计费约束 | 配额约束 | 其他约束 | - -## 约束决策树(mermaid graph TD) -(覆盖主要操作的多层约束检查流程) - -## 特殊实例类型约束汇总 -| 实例/资源类型 | 限制操作 | 原因 | -(✅允许 / ❌禁止 / ⚠️有条件) - -## AI 推理规则速查 -(mermaid 流程图:AI 判断"某操作能否执行"时的逐层检查顺序) -``` - ---- - -### Step 3:生成图谱目录 README - -写入 `{output_dir}/README.md`: -```markdown -# {project_name} 图谱文档集 (Graph RAG) -<!-- search-anchor: 图谱文档, Graph RAG, 关系索引 --> - -## 与主文档体系的关系 -(图谱文档不替代组件文档,而是提供关系视角的结构化索引) - -## 文档目录 -| 文件 | 大小 | 核心内容 | - -## 按问题类型查找 -| 问题类型 | 示例问题 | 查找文档 | -| 依赖关系 | "谁依赖 X?" | G1 组件依赖关系矩阵 | -| 调用链路 | "API X 经过哪些模块?" | G2 调用链路全景 | -| 数据位置 | "数据存在哪里?" | G3 数据流与存储依赖图 | -| 错误排查 | "错误码 XXX 是哪个模块的?" | G4 错误码组件映射表 | -| 场景手册 | "配额检查的完整流程?" | G5 跨组件交互场景手册 | -| 多跳推理 | "A 间接依赖谁?" | G6 知识图谱三元组 | -| 风险评估 | "X 挂了影响多大?" | G7 架构风险与影响面 | -| 配置修改 | "怎么修改 XX 配置?" | G8 核心配置参数索引 | -| 操作约束 | "能不能做 XX?" | G9 业务规则约束矩阵 | - -## 检索路由规则建议 -(关键词 → 优先检索文档) - -## 维护说明 -(组件文档更新后需同步更新图谱文档的时机和范围) -``` - -### Step 4:返回摘要 - -``` -Graph RAG 生成完成: -生成文档: G1~G9 共 9 份 + README - - G1_组件依赖关系矩阵.md: {N}KB,{N}个组件,{N}条关系 - 置信度: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} - - G2_组件调用链路全景.md: {N}KB,{N}条调用链,状态机{N}个状态 - - G3_数据流与存储依赖图.md: {N}KB - - G4_错误码组件映射表.md: {N}KB,{N}段错误码 - - G5_跨组件交互场景手册.md: {N}KB,{N}个场景时序图 - - G6_知识图谱三元组.md: {N}KB,{N}条三元组 - 置信度: EXTRACTED {N} / INFERRED {N} / AMBIGUOUS {N} - - G7_架构风险与影响面分析.md: {N}KB - - G8_核心配置参数索引.md: {N}KB,{N}个配置项 - - G9_业务规则约束矩阵.md: {N}KB -AMBIGUOUS 条目汇总(需人工确认): {N} 处 - - 示例: "Aurora→Compute 通信方式不确定(文档未明确)[A:0.2]" -发现问题: {问题 或 "无"} - -⚠️ 主 Agent 请注意:Graph RAG 完成后,请立即执行 Phase K3 Step 3(跨文档一致性校验)。 -``` - -## 输出 - -``` -<output_dir>/README.md -<output_dir>/G1_{project_name}组件依赖关系矩阵.md -<output_dir>/G2_{project_name}组件调用链路全景.md -<output_dir>/G3_{project_name}数据流与存储依赖图.md -<output_dir>/G4_{project_name}错误码组件映射表.md -<output_dir>/G5_{project_name}跨组件交互场景手册.md -<output_dir>/G6_{project_name}知识图谱三元组.md -<output_dir>/G7_{project_name}架构风险与影响面分析.md -<output_dir>/G8_{project_name}核心配置参数索引.md -<output_dir>/G9_{project_name}业务规则约束矩阵.md -返回摘要字符串 -``` - -## 约束 - -- **关系抽取以组件文档为唯一来源**:不直接读原始代码,防止与 Phase K2 产出不一致 -- **置信度三态强制**:每条关系/三元组必须标注 `EXTRACTED`/`INFERRED`/`AMBIGUOUS`,不得省略 -- **禁止用 0.5 作置信度默认值**:每条关系独立评估分值;INFERRED 直接结构证据 0.8~0.9,命名推断 0.6~0.7,弱证据 0.4~0.5;AMBIGUOUS 用 0.1~0.3 -- **禁止凭空发明关系**:若组件文档无依据,宁可标 AMBIGUOUS 也不捏造 EXTRACTED -- **每份图谱文档必须有 AI 快速理解要点表** -- **每份图谱文档必须有 search-anchor** -- **图谱文档不替代组件文档**:只提供关系视角的结构化索引 -- **状态机必须使用 mermaid stateDiagram-v2** -- **约束决策树必须使用 mermaid graph TD** -- **三元组必须遵循 (Subject, Predicate, Object, Confidence, Score) 格式** -- **操作-状态约束必须是 ✅/❌/⚠️ 矩阵格式** diff --git a/skills/team-wiki-codebase/references/agents/kb-doc-generator.md b/skills/team-wiki-codebase/references/agents/kb-doc-generator.md deleted file mode 100644 index 67ebade0..00000000 --- a/skills/team-wiki-codebase/references/agents/kb-doc-generator.md +++ /dev/null @@ -1,323 +0,0 @@ -# 知识库文档生成 Agent - -## 职责 - -为指定批次的组件/文档类型生成知识库文档,严格遵循九大文档类型规范,确保代码可回溯、AI 快速理解表完整、双向链接织网。 - -**此 Agent 在 Phase K2 中被主 Agent 逐批启动,支持并行子 Agent 分发模式。** - -## 输入包 - -``` -component_list: 本批次待生成的组件名或文档类型列表 - 例如: ["Aurora", "Frame", "CCDB", "Dispatcher"] 或 ["Type-1", "Type-2", "Type-3"] -architecture_map: _review/k1-architecture-map.md 完整内容 -repos: 仓库列表([{name, path, language}]),替代旧的 project_root -service_map: 服务名→仓库映射表(用于跨仓库追踪调用链) -output_dir: 知识库输出根目录 -project_name: 项目名称(用于文档命名,如 "CVM") -product_docs_dir: 产品文档目录(可为空,空则跳过产品约束提取) -methodology_dir: references/methodology/ 目录路径 -completed_docs: 已完成的文档列表(断点恢复时跳过) -parallel_mode: true | false(默认 true;Type-4 组件文档并行,Type-1~3/5~8 串行) -``` - -## 执行步骤 - -### Step 0:加载方法论 - -读取 `{methodology_dir}/phase2-document-types.md`,加载对应文档类型的模板和生成规则。 - -### Step 1:断点检查 - -检查 `completed_docs` 列表,从 `component_list` 中移除已完成项,得到 `pending_list`。 - -若 `pending_list` 为空,直接返回"全部已完成"摘要,不做任何操作。 - -### Step 2:分发策略决策 - -``` -IF component_list 全为 Type-4 组件文档 AND parallel_mode = true: - → 并行模式(Step 2A) -ELSE(Type-1/2/3/5/6/7/8 或 parallel_mode = false): - → 串行模式(Step 2B) -``` - -### Step 2A:并行模式(Type-4 组件文档) - -**MANDATORY:必须使用 Agent tool,禁止一个个顺序处理。** - -**Step 2A-1:分块** - -将 `pending_list` 分成若干块,每块 **3~5 个组件**(组件文档较大,不超过 5 个避免上下文溢出)。 -- 优先把同一架构层的组件放同一块(减少跨层代码读取竞争) -- 已完成的跳过(断点恢复) - -**Step 2A-2:同一条消息并发启动所有子 Agent** - -**在同一次回复中发出所有 Agent tool 调用**。这是并行的唯一方式——分开多次调用则退化为串行。 - -示例(3块并发): -``` -[Agent tool call 1: chunk ["Aurora", "Frame"], subagent_type="general-purpose"] -[Agent tool call 2: chunk ["CCDB", "VSResource"], subagent_type="general-purpose"] -[Agent tool call 3: chunk ["Dispatcher", "Compute"], subagent_type="general-purpose"] -``` - -每个子 Agent 接收以下 prompt(替换 CHUNK_COMPONENTS、CHUNK_NUM、TOTAL_CHUNKS): - -``` -你是 team-wiki-codebase 的组件文档生成子 Agent。 -为以下组件生成知识库文档(chunk CHUNK_NUM / TOTAL_CHUNKS): -CHUNK_COMPONENTS - -架构参考(精简版,仅含本 chunk 相关组件及其直接上下游): -RELEVANT_COMPONENTS_TABLE -(格式:| 组件名 | 架构层级 | 所属仓库 | 语言 | 上游 | 下游 | 入口文件 |) - -服务映射表(用于跨仓库追踪): -SERVICE_MAP_RELEVANT_ENTRIES - -项目信息: -- repos: REPO_LIST(仅列路径,不列详情) -- output_dir: OUTPUT_DIR -- project_name: PROJECT_NAME -- product_docs_dir: PRODUCT_DOCS_DIR(空则跳过产品约束) - -方法论路径: METHODOLOGY_DIR/phase2-document-types.md - -对每个组件执行: -1. 使用 Glob→Grep→Read 三步法扫描代码(参见 kb-doc-generator.md §Step 2:代码结构扫描规范) -2. 提取:核心职责/架构层级/上下游/代码入口/核心机制/数据流向/技术栈/数据模型/配置项 -3. 生成符合 Type-4 模板的文档,Write 到 OUTPUT_DIR/XX_组件名设计说明.md -4. 自校验(见下方 Checklist) -5. 将完成的组件名写入 OUTPUT_DIR/../_review/_chunk_done_CHUNK_NUM.txt(每行一个) - -自校验 Checklist(每份文档生成后): -- [ ] AI 快速理解表 10 维度全部填写且具体(非泛泛描述)? -- [ ] "代码入口"精确到函数名(不是仅文件名)? -- [ ] search-anchor 有 5~15 个关键词? -- [ ] 包含指向主架构文档的双向链接? -- [ ] 无法回溯的内容已标注 [UNVERIFIED]? -- [ ] 无空占位章节? - -[UNVERIFIED] 超过 20% → 文档顶部加 ⚠️ 低可信度警告。 - -无法生成的组件写入 OUTPUT_DIR/../_review/_chunk_failed_CHUNK_NUM.txt 并注明原因。 -``` - -**Step 2A-3:等待并收集结果** - -等待所有子 Agent 完成后: -- 检查 `_chunk_done_N.txt` 文件确认完成情况 -- 若某块 `_chunk_done_N.txt` 不存在,打印警告:`chunk N 可能未完成,检查子 Agent 是否以 general-purpose 类型运行` -- 若超过半数块失败,停止并告知用户重新运行 -- 将所有已完成组件合并到 `progress.json` 的 `kb_progress.components_done` -- 清理临时文件:`rm -f _review/_chunk_done_*.txt _review/_chunk_failed_*.txt` - -### Step 2B:串行模式(Type-1~3/5~8) - -对 `pending_list` 中每个文档类型**顺序执行**(这些文档类型相互依赖,必须串行): - -#### 2B-1:代码结构扫描规范 - -使用 `Glob → Grep → Read` 三步法(**按组件所属仓库的语言自适应**): - -``` -1. Glob:找到组件对应仓库的入口文件(按语言选择模式) - Go: main.go / cmd/*/main.go - Python: main.py / app.py / manage.py / wsgi.py - Java: *Application.java / *Bootstrap.java / src/main/java/**/Main*.java - TypeScript: app.ts / index.ts / main.ts / server.ts - Rust: main.rs / src/main.rs - -2. Grep:定位核心 Handler/Router(按语言+框架选择模式) - Go: grep -rn 'func.*Handler\|\.GET\|\.POST\|router\.\|@handler' <dir> - Python: grep -rn '@app\.\|@router\.\|def.*view\|APIRouter\|include_router' <dir> - Java: grep -rn '@RestController\|@Controller\|@Service\|@GetMapping\|@PostMapping\|@RequestMapping' <dir> - TypeScript: grep -rn 'app\.get\|app\.post\|router\.\|@Get\|@Post\|@Controller' <dir> - Rust: grep -rn '\.route\|\.get\|\.post\|#\[get\|#\[post\|async fn' <dir> - - ⚠️ 排除测试文件:--exclude='*_test.*' --exclude='test_*' --exclude='*_mock.*' - -3. Read:读取核心文件(按 architecture_map 中的目录价值分级) - - ⭐⭐⭐ 必读:业务逻辑层、核心配置文件、DDL - - ⭐⭐ 参考:服务上下文初始化、配置文件 - - ⭐ 可跳过:纯绑定层(通常只是参数透传) - - ✗ 禁止:自动生成文件(*.pb.go, *_gen.go, *_generated.*, node_modules/, target/, build/) -``` - -提取信息(**全部必须有代码文件:行号引用,不得推断**): -- 核心职责(一句话,≤30字) -- 架构层级和上下游组件(通信方式:RPC/MQ/DB) -- 代码入口(文件名 → 核心函数名) -- 核心机制(最重要的1~2个技术机制) -- 数据流向(从哪来 → 经过什么 → 到哪去) -- 技术栈(语言 + 框架 + 中间件) -- 数据模型(涉及的表名 + DDL 关键字段) -- 核心流程(时序图所需的步骤) -- 配置项(配置键 + 默认值 + 影响范围) -- 定时任务(如有) -- 监控指标(如有) - -无法从代码中找到的内容标注 `[UNVERIFIED]`,不得推断。 - -#### 2B-2:产品文档提取(Type-5/6/7,或有 product_docs_dir 时) - -若 `product_docs_dir` 非空: -``` -扫描维度(来自 phase2-document-types.md §Type-5 桥梁文档生成方法): -├── 数量限制(批量上限、配额、最大值) -├── 类型约束(枚举值、互斥关系) -├── 状态前置条件 -├── 计费规则 -├── 安全约束 -└── 兼容性约束 -``` - -将每个产品约束追踪到代码校验位置(`if len() > N` 的具体文件:行号)。 - -#### 2B-3:文档生成 - -按照 `phase2-document-types.md` 中对应类型的模板生成文档。 - -**Type-4 组件文档必须包含(按顺序)**: - -```markdown -# {组件名} 内部设计说明 -<!-- search-anchor: {中文名}, {英文名}, {缩写}, {同义词}, {常见搜索词} --> -> 项目: {project_name} | 代码仓库: {仓库URL} | 架构层级: {层级} -> 在整体架构中的位置: [📘 {project_name} 技术架构 - 4.X {组件名}](./{project_name} 技术架构.md#4x-组件名) - -## 🤖 AI 快速理解要点 -| 维度 | 关键信息 | -|------|---------| -| **核心职责** | {≤30字,具体} | -| **架构层级** | {层级名} → {角色} | -| **上游组件** | {组件A(RPC)}, {组件B(MQ)} | -| **下游组件** | {组件C(RPC)}, {组件D(DB)} | -| **代码入口** | `{文件名}` → `{核心函数名}()` | -| **核心机制** | {机制1};{机制2} | -| **互斥控制** | {并发控制方式,如"分布式锁 key: xx"} | -| **数据流向** | {来源} → {处理} → {去向} | -| **技术栈** | {语言} + {框架} + {中间件} | -| **定时任务** | {N个定时任务,或"无"} | - -## 📋 项目概述 -(核心职责编号列表 + ASCII 架构定位图) - -## 🏗️ 架构设计 -(ASCII 架构图 + 核心子模块说明 + 核心函数签名) - -## 📊 数据模型 -(SQL DDL 含注释 + 数据流向图) - -## 🔌 接口设计 -(对外/对内接口表 + 错误码定义) - -## ⚙️ 核心流程 -(mermaid 时序图 + 步骤说明 + 异常处理) - -## 🔧 配置说明 -(配置项 / 默认值 / 说明 / 影响范围) - -## 📈 监控与告警 - -## 🐛 常见问题与排障 - -## 📝 文档更新记录 -### v1.0 ({日期}) -- ✅ **新增**: 初始版本 -> 代码基准:{commit_sha} ({tag}) -``` - -**所有文档 Write 到 `output_dir` 下,禁止先在对话中打印完整内容再写文件。** - -### Step 3:自校验(准确性验证 + 接口对账) - -每份文档生成后执行,**不得跳过**: - -**结构完整性**: -- [ ] AI 快速理解表 10 个维度全部填写,且每个维度都是具体信息(不是"见下文")? -- [ ] "代码入口"精确到函数名(`文件名:行号 → 函数名()`)? -- [ ] search-anchor 有 5~15 个关键词,包含中英文名和同义词? -- [ ] 包含指向主架构文档的双向链接? -- [ ] 无空占位章节(没有内容的章节直接删除)? - -**接口对账**(仅对 architecture_map 中接口校验类型 ≠ NONE 的组件执行): - -从 `_review/interface-inventory.json` 读取该组件的扫描基准数 `scanned`,统计文档中实际记录的接口数 `documented`: - -``` -HTTP 类型: 统计文档 ## 接口设计 节中列出的路由数 -MQ 类型: 统计文档中明确记录的 Topic/Queue/Exchange 数 -RPC 类型: 统计文档中列出的 RPC Method 数 -``` - -计算差异:`gap = scanned - documented` - -处理规则: -- `gap = 0` → ✅ 接口覆盖完整 -- `0 < gap ≤ 20%` → ⚠️ 少量缺口,在文档末尾加 `<!-- INTERFACE_GAP: 疑似遗漏 N 个接口 -->` -- `gap > 20%` → ❌ 标记 `[INTERFACE_GAP]`,在摘要中注明,建议补充后重跑 - -更新 `progress.json` 中该组件的 `interface_coverage.documented` 字段。 - -**准确性统计**(每份文档单独统计,返回给主 Agent 汇总): -``` -统计方法: - total_claims = 业务规则条数 + 核心流程步骤数 + 接口描述条数 + 配置项条数 - verified = 其中有 file:line 引用的条数 - unverified = 标注了 [UNVERIFIED] 的条数 - ratio = unverified / total_claims -``` - -处理规则: -- `ratio > 20%` → 文档顶部加 `⚠️ 低可信度警告:{unverified}/{total_claims} 项无法回溯到代码` -- `ratio > 40%` → 摘要中标记 **[HIGH_UNVERIFIED]**,建议人工重点确认 - -### Step 4:返回摘要 - -返回给主 Agent(主 Agent 将数据累加到 progress.json 的 `accuracy_stats` 和 `interface_coverage`): - -``` -批次完成摘要: -读取文件: {N} 个(估计 token 消耗: ~{N}k) -生成文档: {N} 份 - -准确性统计: - 总声明数: {N} | 已验证: {N} | [UNVERIFIED]: {N} ({X}%) - -接口对账(仅有接口的组件): - ComponentA [HTTP]: 文档 {M} / 基准 {N} = {X}% ✅/⚠️/❌ - ComponentB [MQ]: 文档 {M} / 基准 {N} = {X}% ✅/⚠️/❌ - -逐文档明细: - - {组件名}设计说明.md: {N}KB,声明{N}条,[UNVERIFIED]{N}条({X}%) [HIGH_UNVERIFIED/INTERFACE_GAP 如适用] - -跳过(已完成): {N} 份 -发现问题: {问题描述 或 "无"} -``` - -## 输出 - -``` -<output_dir>/XX_{组件名}设计说明.md ← Type-4 组件文档 -<output_dir>/{project_name} 技术架构.md ← Type-1(如本批次包含) -<output_dir>/{project_name} 业务架构.md ← Type-2 -<output_dir>/{project_name} 部署架构.md ← Type-3 -<output_dir>/XX_{project_name}核心API产品代码映射.md ← Type-5 -<output_dir>/XX_{project_name}产品规则速查表.md ← Type-6 -<output_dir>/XX_{project_name}业务开发规范SOP.md ← Type-7 -<output_dir>/{知识增强文档}.md ← Type-8 -返回摘要字符串 -``` - -## 约束 - -- **代码为真**:所有描述必须有代码文件引用,不可验证内容必须标注 `[UNVERIFIED]` -- **模板强制**:生成每类文件前必须先读取对应章节的模板 -- **严禁空文档**:没有实质内容则不创建文件 -- **严禁冗余输出**:直接 Write 文件,不在对话中打印完整内容 -- **命名规范**:组件文档用 `XX_{组件名}设计说明.md`,XX 按依赖链顺序分配(底层组件编号小) -- **API 未提供时**:Type-5/6 可跳过产品约束映射,将约束值标注为 `[PRODUCT_DOC_MISSING]` diff --git a/skills/team-wiki-codebase/references/methodology/phase0-collection.md b/skills/team-wiki-codebase/references/methodology/phase0-collection.md deleted file mode 100644 index cf240913..00000000 --- a/skills/team-wiki-codebase/references/methodology/phase0-collection.md +++ /dev/null @@ -1,54 +0,0 @@ -# Phase 0: 源材料采集与预处理 - -## 仓库发现与分类 - -从入口仓库出发,递归发现所有相关仓库: - -1. **依赖分析**: 解析项目依赖文件(如 `requirements.txt`, `package.json`, `pom.xml`, `Cargo.toml`, `go.mod` 等,按检测到的语言选择) -2. **配置引用**: 解析流程编排配置中引用的模块名 → 仓库映射 -3. **RPC 服务发现**: 从服务注册配置提取服务名 → 仓库映射 -4. **按架构层级分类**: API接入层 / 流程引擎层 / 服务执行层 / 资源调度层 / 数据适配层 / 基础执行层 -5. **标记核心度**: 根据代码行数、被依赖数、Handler 数量计算优先级 - -## 关键文件提取清单 - -| 文件类型 | 匹配模式 | 提取目的 | -|---------|---------|---------| -| **入口文件** | `main.py`, `main.go`, `cmd/*/main.go`, `app.ts` | 服务启动方式和初始化流程 | -| **路由/Handler** | `handler.*`, `router.*`, `controller.*` | API 接口和消息处理入口 | -| **配置文件** | `*config*.*`, `conf/`, `*.yaml`, `*.toml` | 流程编排、参数配置 | -| **Proto/IDL** | `*.proto`, `*.thrift`, `*schema*` | RPC 接口契约和数据结构 | -| **数据库操作** | `*db*.*`, `*dao*.*`, `*model*.*`, `*repository*.*` | 数据模型和表结构 | -| **常量/错误码** | `*const*`, `*error*`, `*code*`, `*enum*` | 错误码体系和业务常量 | -| **测试文件** | `*_test.*`, `test_*.*` | 预期行为和边界条件 | - -## 构建代码知识图谱 - -在正式生成文档前,构建代码知识图谱作为中间表示: - -**节点类型**: `[Service]` / `[Handler]` / `[Config]` / `[Table]` / `[Queue]` / `[API]` / `[ErrorCode]` - -**边类型**: `[CALLS]`(同步RPC/HTTP) / `[PUBLISHES]`(异步MQ) / `[CONSUMES]`(MQ消费) / `[READS]`(DB读) / `[WRITES]`(DB写) / `[CONFIGURES]`(配置驱动) / `[MAPS_TO]`(产品→代码) - -**构建方法**(按可用性排序): -1. **`teamai codebase --extract`** — Tree-sitter 结构边(**TS/JS/Python/Go** 等)+ 多语言 heuristic 事实页(writes `teamwiki/`) -2. Grep + Read(Agent K1/K2)— 补充动态路由、配置驱动调用 -3. 解析编排配置 → 模块→命令映射 -4. 解析 Proto/IDL/DDL → 数据结构和表关系(结构化文件,可精确解析) -5. MQ 拓扑推断 → Exchange/Topic/Queue/Routing Key -6. API 映射 → 外部 API 名称 → 内部 Handler 入口 - -> `code-ast` 对相对 import 可产出 `DEPENDS_ON` 边;包级/动态调用仍可能遗漏,标 `[UNVERIFIED]` 或 `AMBIGUOUS`。 -> AST 结果优先于 heuristic。There is no separate capabilities doc in this package; use `teamai codebase --extract` output under `teamwiki/`. - -## 输入源优先级 - -| 优先级 | 输入源 | 具体内容 | 产出文档类型 | -|--------|--------|---------|------------| -| **P0 必须** | 代码仓库 | 目录结构、入口文件、配置、Proto | Type-1,4 | -| **P0 必须** | 流程编排配置 | workflow_config / 状态机 | Type-1,4,5 | -| **P0 必须** | 产品 API 文档 | 接口参数、错误码 | Type-5,6 | -| **P1 重要** | 数据库 Schema | DDL、表结构 | Type-4 | -| **P1 重要** | 产品使用文档 | 使用限制、FAQ | Type-6,8a | -| **P2 增强** | Git 历史 | Commit/MR 记录 | Type-8b | -| **P2 增强** | 故障记录 | 事故报告 | Type-8d | diff --git a/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md b/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md deleted file mode 100644 index 969c25cb..00000000 --- a/skills/team-wiki-codebase/references/methodology/phase1-reverse-engineering.md +++ /dev/null @@ -1,89 +0,0 @@ -# Phase 1: 架构逆向工程 — 从代码到架构认知 - -## 1. 自底向上分层法 - -``` -Step 1: 识别"叶子节点" — 直接操作基础设施 - ├── 数据库操作 (MySQL/PostgreSQL/Redis/MongoDB) - ├── 消息队列操作 (RabbitMQ/Kafka/RocketMQ) - ├── 外部系统调用 (第三方 API / 底层驱动) - └── 文件/对象存储操作 (S3/OSS/COS) - -Step 2: 识别"中间节点" — 编排和路由 - ├── 消息路由框架 (消费者路由分发) - ├── 任务调度器 (定时任务/延迟任务) - ├── 流程编排引擎 (Workflow/Saga/状态机) - └── 资源调度器 (负载均衡/资源分配) - -Step 3: 识别"根节点" — 外部入口 - ├── API 网关 / HTTP Handler / gRPC Server - ├── 定时任务入口 (Cron/Scheduler) - └── 事件监听入口 (Webhook/EventBus) - -Step 4: 按调用方向分层 - 外部入口 → 流程编排 → 服务执行 → 资源调度 → 数据操作 → 基础设施 -``` - -### 分层判定规则 - -| 判定特征 | 所属层级 | 典型代码模式 | -|---------|---------|-------------| -| HTTP/gRPC Server 启动 | API 接入层 | `http.ListenAndServe()`, `grpc.NewServer()` | -| 参数校验 + 鉴权 + 限流 | API 接入层 | `validate()`, `auth()`, `rateLimit()` | -| 流程步骤配置和状态机 | 流程引擎层 | `workflow_config`, `state_machine` | -| MQ 消费 + Handler 路由 | 服务执行层 | `channel.consume()`, `handler.dispatch()` | -| 调度算法 (Filter/Score) | 资源调度层 | `filter()`, `score()`, `schedule()` | -| DB CRUD + 缓存操作 | 数据适配层 | `db.query()`, `redis.get()` | -| 底层系统调用/驱动 | 基础执行层 | `exec()`, `syscall.*`, `driver.*` | - -## 2. 三层穿透追踪法(核心方法论) - -对任何用户可见 API 操作,完成三层穿透追踪: - -``` -Layer 1: API 入口层 - ├── 定位 Handler 函数 - ├── 提取参数校验逻辑 - ├── 识别硬编码默认值和白名单 - └── 确定下游调用方式 (同步RPC / 异步MQ) - -Layer 2: 流程编排层 - ├── 查找流程配置 (workflow_config / saga_config) - ├── 解析步骤序列 (步骤名/执行模块/回滚模块/超时/重试) - ├── 标注每步的执行模块和回滚模块 - └── 确定步骤间的数据传递方式 - -Layer 3: 服务执行层 - ├── 追踪每个步骤的具体 Handler 实现 - ├── 识别数据库操作和状态变更 - ├── 标注外部系统调用 - └── 确定最终执行结果的回调路径 - -输出: 完整调用链时序图 + 状态流转图 + 数据流向图 -``` - -### 调用链文档化标准格式 - -``` -[API名称](代码入口: {仓库}/{路径}/{文件}) - → 参数校验 + 鉴权限流 - → [前置检查]: {检查内容} - → RPC/MQ → [编排层] ({配置文件}: {操作名}) - → [服务层] ({配置文件}: {flow_name}) - → [{步骤1模块}] {步骤1命令} ({具体说明}) - → [{步骤2模块}] {步骤2命令} ({具体说明}) - → ... - → 回调 [编排层] -``` - -## 3. 组件关系矩阵 - -构建 N×N 关系矩阵,标注通信方式: - -| 调用方 ↓ / 被调方 → | 组件A | 组件B | 组件C | -|---------------------|-------|-------|-------| -| **组件A** | — | RPC | MQ | -| **组件B** | — | — | DB | -| **组件C** | RPC | MQ | — | - -标注: `RPC`(同步) / `MQ`(异步) / `DB`(共享数据库) / `—`(无直接通信) diff --git a/skills/team-wiki-codebase/references/methodology/phase2-document-types.md b/skills/team-wiki-codebase/references/methodology/phase2-document-types.md deleted file mode 100644 index fddd0ac3..00000000 --- a/skills/team-wiki-codebase/references/methodology/phase2-document-types.md +++ /dev/null @@ -1,341 +0,0 @@ -# Phase 2: 九大文档类型生成规范与模板 - -## Type-1: 技术架构总览 - -**规模**: ~200KB | **数量**: 1 份 - -### 必备章节 - -``` -读者导航指南 (按角色推荐阅读路径) -知识库检索路由指引 (AI 专用,4条分流规则+4级优先级) -1. 架构概述 (30秒快速理解表、整体架构图ASCII、组件关系矩阵) -2. 三维架构视图 (逻辑/数据/部署) -3. 核心链路 ⭐ (每条核心API的完整时序图+调用链) -4. 核心组件详解 (每组件概述+表格) -5. 配置管理与服务发现 -6. 数据模型与存储架构 ⭐ -7. 高可用与技术架构 -8. 架构演进与设计决策 -9. AI 研发知识库规范 ⭐ (元数据QA/全局状态机/MQ拓扑/调度引擎/跨层追踪) -附录: 代码仓库/术语表/代码入口索引/错误码 -``` - -### 生成规则 -- T1-R01: 必须包含读者导航指南 -- T1-R02: 必须包含 AI 检索路由规则 -- T1-R03: 核心链路必须有时序图 -- T1-R04: 组件表必须包含代码仓库列 -- T1-R05: 术语表必须包含内外部映射 -- T1-R06: 必须有 AI 专用第 9 章 -- T1-R07: 架构图使用 ASCII Art - ---- - -## Type-2: 业务架构文档 - -**规模**: ~70KB | **数量**: 1 份 - -``` -1. 产品能力矩阵 (能力域/子能力/对应API/计费影响) -2. 计费模型详解 (模式对比/状态机/退费续费规则) -3. 核心实体生命周期 (完整状态机/各状态允许操作/互斥规则) -4. 核心业务流程 (用户视角时序图+前置条件+异常处理) -5. 产品规格体系 (命名规则/规格与底层资源映射) -``` - ---- - -## Type-3: 部署架构文档 - -**规模**: ~40KB | **数量**: 1 份 - -``` -1. 分层部署架构图 -2. 服务部署矩阵 (服务名/部署方式/实例数/资源配置/依赖) -3. 环境配置 (生产/测试/差异对照) -4. 部署流程与变更管理 -``` - ---- - -## Type-4: 组件设计文档(核心产出) - -**规模**: 20~100KB/份 | **数量**: N 份(每组件一份) - -### 标准模板 - -``` -# {组件名} 内部设计说明 -<!-- search-anchor: 组件名, 别名, 核心关键词 --> -> 项目名称 / 版本 / 代码仓库 / 代码规模 -> 在整体架构中的位置: [📘 链接到主架构文档] - -## 🤖 AI 快速理解要点 -(10 维度结构化摘要,详细定义见 [phase3-ai-enhancement.md §1](phase3-ai-enhancement.md)) - -## 📋 项目概述 (核心职责+在架构中的位置) -## 🏗️ 架构设计 (ASCII架构图+核心子模块,函数签名) -## 📊 数据模型 (SQL DDL带注释+数据流向图) -## 🔌 接口设计 (对外接口表+对内接口+错误码) -## ⚙️ 核心流程 (时序图+步骤说明+异常处理) -## 🔧 配置说明 (配置项/默认值/说明/影响范围) -## 📈 监控与告警 -## 🐛 常见问题与排障 -``` - -### 生成规则 -- T4-R01: 必须有 AI 快速理解表 -- T4-R02: 必须有双向链接到主架构文档 -- T4-R03: 核心函数必须列出签名 -- T4-R04: SQL DDL 必须包含注释 -- T4-R05: 配置项必须标注影响范围 -- T4-R06: 架构图使用 ASCII Art -- T4-R07: 代码入口必须精确到函数名 - -### 从代码生成的步骤 - -> 详细执行规范见 `references/agents/kb-doc-generator.md`,此处仅列概要: -> 1. 代码结构扫描(Glob → Grep → Read 三步法,按语言自适应) -> 2. 信息提取(10 维度:核心职责/架构层级/上下游/代码入口/核心机制/数据流向/技术栈/数据模型/配置项/定时任务) -> 3. 文档组装(按上述模板章节顺序) -> 4. 自校验(准确性统计 + 接口对账) - ---- - -## Type-5: 产品-代码映射(桥梁文档) - -### 每个核心 API 一节 - -``` -### N.1 用户意图 (一句话) -### N.2 产品约束 (约束项/约束值/影响组件/校验位置) -### N.3 用户可见状态流转 (ASCII图+内部状态映射) -### N.4 内部调用链路 (标准格式精确到代码文件) -### N.5 写代码时必须考虑的 (硬性约束编号列表) -### N.6 错误码与内部异常映射 (外部码/内部组件/含义) -``` - -### 生成规则 -- T5-R01: 约束表必须标注"影响的组件"和"校验位置" -- T5-R02: 调用链必须精确到代码文件路径 -- T5-R03: 状态流转必须标注内部状态码映射 -- T5-R04: "写代码时必须考虑的"是强制章节 -- T5-R05: 错误码映射必须包含内部组件归属 - -### 桥梁文档生成方法(3 Step) - -**Step 1: 提取产品约束** — 从产品文档中提取所有影响代码实现的约束: - -``` -扫描维度: -├── 数量限制 (批量上限、配额、最大值) -├── 类型约束 (枚举值、互斥关系) -├── 状态前置条件 (操作前资源必须处于什么状态) -├── 计费规则 (不同计费模式的差异处理) -├── 安全约束 (鉴权、加密、脱敏) -└── 兼容性约束 (类型兼容、版本兼容、地域限制) -``` - -**Step 2: 映射到代码位置** — 对每个产品约束,追踪到代码中的具体校验位置: - -``` -产品约束: "{API名} 批量上限 N" - ↓ 追踪 -代码位置: {API网关组件} → {文件路径} → validate_params() - ↓ 确认 -校验方式: if len(resource_ids) > N: raise InvalidParameterValue -``` - -**Step 3: 构建映射表** — 将上述信息组装为标准的产品-代码映射表(见 Type-5 模板)。 - -**桥梁文档质量标准**: - -| 质量维度 | 标准 | 检查方法 | -|---------|------|---------| -| **完整性** | 所有核心 API 都有映射 | 对照 API 列表逐一检查 | -| **精确性** | 代码路径精确到文件和函数 | 实际打开代码验证 | -| **一致性** | 约束值与产品文档一致 | 交叉比对产品文档 | -| **时效性** | 与最新代码版本同步 | 定期 diff 检查 | - ---- - -## Type-6: 产品规则速查表 - -``` -## N. {规则类别} -| 规则 | 约束值 | 影响的组件 | 校验位置 | 来源文档 | - -## 状态与操作互斥规则 -| 当前状态 | 允许的操作 | 禁止的操作 | -``` - -- T6-R01: 每条规则必须标注"影响的组件" -- T6-R02: 约束值必须是精确数字 -- T6-R03: 必须有"来源文档"列 -- T6-R04: 状态互斥规则必须是完整矩阵 - ---- - -## Type-7: 业务开发规范 SOP - -``` -1. 为什么需要标准代码模板 (野生代码问题) -2. 核心规约 (绝不向外暴露底层错误/Context一传到底/参数前置校验) -3. 标准 Handler 代码模板 (可直接复制,标注"AI 编码铁律") -4. 错误码映射对照表 (场景描述用AI思考逻辑/推荐错误码/Message) -5. AI 评审 CheckList (可机器校验) -``` - -- T7-R01: 代码模板必须可直接复制运行 -- T7-R02: 每个关键注释标注"AI 编码铁律" -- T7-R03: 错误码表用"AI的思考逻辑"作为场景描述 - ---- - -## Type-8: 知识增强文档 - -### Type-8a: 产品知识文库 -标注 `type: bridge`,表格对比易混淆概念,含"代码传参示例"和"架构与业务影响"列。 - -### Type-8b: 反模式与踩坑指南 -五段式:**触发场景→错误表现→根因分析→正确做法→关联组件** -概览表标注编号/分类/严重程度(P0致命/P1严重/P2重要)/关联组件。 - -### Type-8c: RPC 接口契约 -struct 定义含序列化 Tag + 必填/选填标注 + AI 编码契约要求。 - -### Type-8d: 排障案例记录 (Memorix) -结构:问题现象→排查过程(Step N)→根因定位→修复方案→经验总结→关联文档。 - ---- - -## Type-9: 图谱文档集(Graph RAG) - -**规模**: 10~30KB/份 | **数量**: 5~10 份 | **目录**: `graph/` - -> 将散落在 N 份组件文档中的**跨组件关系信息**抽取为结构化索引,解决 RAG 检索在关系查询场景下的"信息分散"问题。 - -### 图谱文档类型清单 - -| 编号 | 文档名 | 核心内容 | 解决的检索痛点 | -|------|--------|---------|--------------| -| G1 | 组件依赖关系矩阵 | N×N 通信矩阵 + 正向/反向依赖索引 + 外部服务依赖 | "谁依赖 X?" 需遍历所有文档 | -| G2 | 组件调用链路全景 | 核心 API 端到端链路 + 读写分离机制 + **完整状态机流转图** + 操作-状态约束矩阵 | "API 经过哪些模块?" 信息分散 | -| G3 | 数据流与存储依赖图 | 存储依赖矩阵 + MQ 队列拓扑 + 缓存策略 | "数据存在哪里?" | -| G4 | 错误码组件映射表 | 错误码段分配 + 外部→内部映射 | "错误码是哪个模块的?" | -| G5 | 跨组件交互场景手册 | ≥10 个场景的 mermaid 时序图 + 异常处理 | "配额检查怎么做的?" | -| G6 | 知识图谱三元组 | (S, P, O) 三元组 + 多跳依赖路径索引 | "A 间接依赖谁?" | -| G7 | 架构风险与影响面分析 | 爆炸半径 + 聚类分析 + 关键路径/瓶颈 | "X 挂了影响多大?" | -| G8 | **核心配置参数索引** | 分层配置项→行为影响映射 + 变更影响面速查 | "怎么修改 XX 配置?" | -| G9 | **业务规则约束矩阵** | 操作前置条件 + 硬件/迁移/计费约束 + AI 推理决策树 | "能不能做 XX?" | - -### 图谱文档生成规则 - -- T9-R01: 每份图谱文档必须有 `🤖 AI 快速理解要点` 表 -- T9-R02: 每份图谱文档必须有 `<!-- search-anchor: ... -->` 锚点 -- T9-R03: 图谱目录必须有 `README.md` 索引,含"按问题类型查找"表和"检索路由规则建议" -- T9-R04: 状态机必须使用 mermaid `stateDiagram-v2` 格式 -- T9-R05: 约束决策树必须使用 mermaid `graph TD` 格式 -- T9-R06: 操作-状态约束必须是 ✅/❌ 矩阵格式 -- T9-R07: 配置参数必须标注"影响行为"、"变更风险"(🟢低/🟡中/🔴高)、"生效方式"(热生效/需重启) -- T9-R08: 业务规则约束必须包含 AI 推理检查流程(mermaid 流程图) -- T9-R09: 三元组必须遵循 (Subject, Predicate, Object) 标准格式 -- T9-R10: 图谱文档**不替代**组件文档,而是提供**关系视角的结构化索引** - -### 图谱文档生成方法 - -**Step 1: 关系抽取** — 从 N 份组件文档中提取跨组件关系: - -``` -扫描维度: -├── 调用关系 (A calls B, 协议, 场景) -├── 数据依赖 (A reads/writes B, 数据内容) -├── 消息拓扑 (A publishes_to/consumes_from Queue) -├── 状态流转 (操作 → 起始状态 → 中间状态 → 终态) -├── 约束条件 (操作 → 前置条件 → 硬件/计费/配额约束) -├── 配置映射 (配置项 → 影响行为 → 变更风险) -└── 错误码归属 (错误码段 → 组件 → 排查方向) -``` - -**Step 2: 结构化建模** — 将抽取的关系转化为标准格式: - -``` -关系矩阵 → N×N 表格 -调用链路 → 端到端文本链路 + mermaid 时序图 -状态机 → mermaid stateDiagram-v2 -约束规则 → 决策树(mermaid graph TD) + 汇总表 -配置索引 → 分层表格(配置项/默认值/影响行为/变更风险/生效方式) -三元组 → (Subject, Predicate, Object, Protocol, Scenario) 表格 -``` - -**Step 3: 索引织网** — 建立图谱文档间的交叉引用和检索路由: - -``` -README.md: -├── 文档目录表 (文件/大小/核心内容) -├── 按问题类型查找表 (问题类型/示例/查找文档) -└── 检索路由规则建议 (关键词→优先检索文档) -``` - -### 关键模板 - -#### 状态机流转图模板 - -```markdown -## 实例状态机完整流转图 - -### 核心状态流转图 -​```mermaid -stateDiagram-v2 - [*] --> PENDING: CreateAction - PENDING --> RUNNING: 创建成功 (flag: 2→1) - RUNNING --> STOPPING: StopAction (flag: 1→8) - STOPPING --> STOPPED: 关机成功 (flag: 8→3) - ... -​``` - -### 操作-状态约束速查矩阵 -| 操作 \ 当前状态 | RUNNING | STOPPED | PENDING | ... | -|---------------|:-------:|:-------:|:-------:|:---:| -| **Start** | ❌ | ✅ | ❌ | ... | -| **Stop** | ✅ | ❌ | ❌ | ... | -``` - -#### 业务规则约束矩阵模板 - -```markdown -## 操作前置条件矩阵 -| 操作 | 状态要求 | 硬件约束 | 计费约束 | 配额约束 | 其他约束 | - -## 迁移约束决策树 -​```mermaid -graph TD - A[迁移请求] --> B{硬件约束1?} - B -->|是| C["❌ 禁止"] - B -->|否| D{硬件约束2?} - ... -​``` - -## AI 推理规则速查 -​```mermaid -graph TD - A["用户问:能否执行 XX?"] --> B["Step 1: 状态检查"] - B --> B1{"查操作-状态约束矩阵"} - B1 -->|❌| Z1["不能,状态不支持"] - B1 -->|✅| C["Step 2: 类型检查"] - ... -​``` -``` - -#### 配置参数索引模板 - -```markdown -## {组件层}配置参数 -| 配置项 | 默认值 | 影响行为 | 变更风险 | 生效方式 | -|--------|--------|---------|---------|---------| -| `config.key` | value | 描述 | 🟢低/🟡中/🔴高 | 热生效/需重启 | - -## 配置变更影响面速查 -| 变更类型 | 影响范围 | 生效方式 | 回滚策略 | 变更风险 | -``` diff --git a/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md b/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md deleted file mode 100644 index 8ebd799b..00000000 --- a/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md +++ /dev/null @@ -1,164 +0,0 @@ -# Phase 3: AI-Native 增强 — 让知识库对 AI 可理解 - -## 1. AI 快速理解表(每份组件文档必备) - -RAG 检索返回的 chunk 通常是文档片段。AI 快速理解表确保无论检索到文档哪个部分,AI 都能在表头获得组件全局上下文。 - -```markdown -## 🤖 AI 快速理解要点 - -| 维度 | 关键信息 | -|------|---------| -| **核心职责** | {一句话,不超过 30 字} | -| **架构层级** | {所属层级} → {在层级中的角色} | -| **上游组件** | {组件名(通信方式)} | -| **下游组件** | {组件名(通信方式)} | -| **代码入口** | {入口文件} → {核心函数} | -| **核心机制** | {最重要的 1-2 个技术机制} | -| **互斥控制** | {并发控制方式} | -| **数据流向** | {从哪来 → 经过什么 → 到哪去} | -| **技术栈** | {语言 + 框架 + 中间件} | -| **定时任务** | {N 个定时任务(简述核心任务)} | -``` - -规则: -- 每个维度必须是**具体的**,不能泛泛描述 -- "代码入口"精确到 `文件名 → 函数名` -- "上下游组件"必须标注通信方式 (RPC/MQ/DB) -- 表格放在文档最前面(紧跟标题之后) - -## 2. 检索路由规则(主架构文档必备) - -防止 RAG 检索内外部文档"串台": - -```markdown -## 知识库检索路由指引(AI 专用) - -### 文档分类总览 -| 分类 | 目录位置 | 文档数量 | 内容性质 | -| 【内部·桥梁】产品-代码映射 | ... | N 份 | 核心API意图→约束→链路 | -| 【内部】组件设计文档 | ... | N 份 | 架构设计、代码入口 | -| 【外部】产品 API 文档 | ... | N 份 | 官网 API 参考 | - -### 检索路由规则 -规则 1 — 内部架构优先: 涉及组件名/内部概念 → 仅检索内部文档 -规则 2 — 外部文档适用: 涉及 API 参数/产品限制 → 检索外部文档 -规则 3 — 混合查询: 同时涉及 → 优先内部,辅以外部 -规则 4 — 写代码前先查约束: 必须先检索桥梁文档 - -### 文档优先级 -| 一级(核心) | 产品-代码映射 + 规则速查表 | 写代码前必查 | -| 二级(架构) | 组件设计文档 + 主架构文档 | 理解内部实现 | -| 三级(业务) | 业务架构 + 核心链路 | 理解业务流程 | -| 四级(备查) | 外部 API 原始文档 | 仅在上述不能回答时 | -``` - -## 3. Search Anchor(语义检索锚点) - -每份文档标题下方添加: - -```html -<!-- search-anchor: 关键词1, 关键词2, 同义词, 英文术语, 中文术语 --> -``` - -- 包含: 中文名、英文名、缩写、同义词、常见搜索词 -- 数量: 5~15 个 -- 示例: `<!-- search-anchor: RPC契约, Schema, 接口契约, Protobuf, IDL -->` - -## 4. 双向链接织网 - -```markdown -# 组件文档 → 主架构文档 -> 在整体架构中的位置: [📘 主架构文档 - 4.5 {组件名}](./主架构文档.md#45-组件名) - -# 主架构文档 → 组件文档 -详见 [{组件名}设计说明](./XX_{组件名}设计说明.md) - -# 桥梁文档 → 组件文档 -| [{组件名}](./XX_{组件名}设计说明.md) | 入参校验层 | -``` - -织网规则: -1. 每份组件文档 ≥ 1 个链接指向主架构文档 -2. 主架构文档每个组件提及处有链接指向组件文档 -3. 桥梁文档中提到的每个组件有链接 -4. 反模式文档的"关联组件"有链接 - -## 5. QA 对生成(AI 元数据层) - -在主架构文档 AI 专用章节预置高频 QA 对(10~20 个): - -```markdown -- **Q: 核心实体的状态机是如何定义的?** - A: 见 `3.7 实体完整状态机` 及 `9.2.1 全局状态一致性映射表`。 - -- **Q: 流程步骤配置在哪里?异常如何补偿回滚?** - A: 采用 N 级编排。宏观流程在 {配置文件1},细粒度步骤在 {配置文件2}。 - -- **Q: 消息队列的拓扑和路由规则?** - A: 见 `9.3.1 MQ 路由拓扑`。核心 Exchange/Topic 包括 {列表}。 - -- **Q: 资源互斥(加锁)规范?** - A: 见 `9.4.4 分布式锁与幂等规范`。使用 {锁方案}。 -``` - -每个 A 必须包含具体的章节/文档引用。 - -## 6. 图谱文档 AI 增强规范 - -图谱文档是 AI-Native 知识库的**关系索引层**,专门解决 RAG 在"跨组件关系查询"场景下的检索失败问题。 - -### 6.1 图谱文档 README 必备结构 - -```markdown -# 图谱文档集 (Graph RAG) -## 与主文档体系的关系 (三层定位表) -## 文档目录 (文件/大小/核心内容) -## 按问题类型查找 (问题类型/示例/查找文档) -## 检索路由规则建议 (关键词→优先检索文档) -## 维护说明 -``` - -### 6.2 图谱文档 AI 快速理解表 - -每份图谱文档必须在标题后紧跟: - -```markdown -## 🤖 AI 快速理解要点 -| 维度 | 关键信息 | -|------|---------| -| **文档定位** | {一句话定位} | -| **核心价值** | {AI 用这份文档能做什么} | -| **覆盖范围** | {覆盖了哪些实体/关系} | -| **使用场景** | {典型问题示例} | -| **与状态机的关系** | {如适用:状态机解决X,本文档解决Y} | -``` - -### 6.3 AI 推理规则嵌入 - -对于约束类图谱文档,必须嵌入 AI 推理决策流程: - -```markdown -## AI 推理规则速查 -> AI 判断"某操作能否执行"时,按以下优先级逐层检查: - -1. **状态检查** → 查操作-状态约束矩阵 -2. **类型检查** → 查特殊实例类型约束汇总 -3. **硬件检查** → 查硬件约束详表 -4. **计费检查** → 查计费约束详表 -5. **配额检查** → 查产品规则速查表 -6. **互斥检查** → 是否有进行中的操作 -``` - -### 6.4 配置变更检查清单 - -对于配置类图谱文档,AI 回答"怎么修改 XX 配置"时必须同时告知: - -``` -1. 配置文件位置 — 在哪个文件/仓库中 -2. 影响范围 — 全地域还是单地域/单机 -3. 生效方式 — 热生效还是需要重启 -4. 回滚策略 — 如何快速回滚 -5. 变更风险 — 🟢低 / 🟡中 / 🔴高 -6. 灰度建议 — 是否需要灰度发布 -``` diff --git a/skills/team-wiki-codebase/references/methodology/phase4-quality.md b/skills/team-wiki-codebase/references/methodology/phase4-quality.md deleted file mode 100644 index a28d3e3f..00000000 --- a/skills/team-wiki-codebase/references/methodology/phase4-quality.md +++ /dev/null @@ -1,232 +0,0 @@ -# Phase 4: 质量评估与迭代优化 - -> 辅助工具: `scripts/validate_kb.py` — 自动校验链接完整性、anchor 覆盖率、AI 快速理解表覆盖率、双向链接、README 索引收录率 - -## 五维评估模型 - -| 维度 | 权重 | 达标标准 | -|------|------|---------| -| **覆盖率** | 25% | ≥ 90% 核心组件有文档 | -| **深度** | 25% | ≥ 80% 代码入口可直接定位 | -| **一致性** | 20% | 0 死链接,0 矛盾描述 | -| **AI 可用性** | 20% | RAG 检索准确率 ≥ 85% | -| **时效性** | 10% | 核心文档更新滞后 ≤ 30 天 | - -## 覆盖率检查 - -``` -□ 每个代码仓库有对应的组件设计文档? -□ 每个核心 API 有产品-代码映射? -□ 每个数据表在某份文档中有 Schema 说明? -□ 每个 MQ Exchange/Topic/Queue 在拓扑图中标注? -□ 每个错误码在映射表中? -□ 每个配置项在配置说明中? -□ 每个定时任务在某份文档中说明? -``` - -## RAG 检索测试用例 - -| 测试类型 | 示例问题 | 期望命中 | -|---------|---------|---------| -| 组件定位 | "{组件名}的代码入口在哪?" | 组件设计文档 | -| 流程追踪 | "{API名}的内部调用链路?" | 产品-代码映射 | -| 约束查询 | "{操作}的批量上限?" | 规则速查表 | -| 状态查询 | "处于{状态}时可执行什么操作?" | 状态互斥规则 | -| 错误排查 | "遇到{错误码}怎么排查?" | 反模式/排障记录 | -| 代码生成 | "写一个{功能}的 Handler" | SOP + 接口契约 | -| 概念辨析 | "{A}和{B}区别?" | 产品知识文库 | - -## 增量更新触发表 - -| 触发条件 | 更新动作 | -|---------|---------| -| 新增代码仓库 | 生成 Type-4 组件文档 | -| API 接口变更 | 更新 Type-5 映射 + Type-6 速查表 | -| 新增产品功能 | 更新 Type-2 业务架构 + Type-8a 知识文库 | -| 线上故障 | 新增 Type-8d 排障记录 + 更新 Type-8b 反模式 | -| 架构重构 | 更新 Type-1 架构总览 + 受影响 Type-4 | -| 配置变更 | 更新对应组件文档的配置章节 | - -## 版本管理规范 - -每份文档底部维护变更记录: - -```markdown -## 📝 文档更新记录 - -### vX.Y (YYYY-MM-DD) -- ✅ **新增**: {新增内容描述} -- ✅ **修复**: {修复内容描述} -- ✅ **更新**: {更新内容描述} -- ⚠️ **废弃**: {废弃内容描述} -``` - -## 常见质量问题修复 - -| 问题 | 修复方法 | -|------|---------| -| 死链接 | 全局 grep `](` 链接,或运行 `scripts/validate_kb.py` | -| 术语不一致 | 建立术语表全局替换 | -| 代码入口过时 | 定期与代码仓库 diff | -| 约束值过时 | 定期与产品文档交叉比对 | -| AI 检索失败 | 补充 search-anchor 关键词 | -| 文档孤岛 | 补充双向链接 | - ---- - -## 完整生成流水线 Checklist - -### Phase 0 Checklist: 源材料采集 - -``` -□ 所有核心代码仓库已克隆 -□ 产品 API 文档已采集 (接口名/入参/出参/错误码) -□ 产品使用文档已采集 (使用限制/FAQ/计费说明) -□ 数据库 Schema 已提取 (DDL/表结构) -□ 流程编排配置已提取 (workflow_config 等) -□ Proto/IDL 文件已提取 -□ 错误码定义已提取 -``` - -### Phase 1 Checklist: 架构逆向工程 - -``` -□ 代码知识图谱已构建 (节点+边) -□ 架构分层已确定 (≥4 层) -□ 组件关系矩阵已构建 (N×N) -□ 核心调用链已追踪 (≥5 条核心 API) -□ MQ 拓扑已推断 (Exchange/Topic/Queue/Routing Key) -□ 数据库 ER 模型已构建 -□ 术语表已整理 (内外部映射) -``` - -### Phase 2 Checklist: 文档生成 - -``` -□ [Type-1] 技术架构总览文档 (1份) - □ 包含读者导航指南 - □ 包含 AI 检索路由规则 - □ 包含核心链路时序图 (≥5 条) - □ 包含组件关系矩阵 - □ 包含 AI 专用第 9 章 - □ 包含术语表 - -□ [Type-2] 业务架构文档 (1份) - □ 包含产品能力矩阵 - □ 包含计费模型(如适用) - □ 包含核心实体生命周期状态机 - -□ [Type-3] 部署架构文档 (1份) - □ 包含服务部署矩阵 - □ 包含环境配置 - -□ [Type-4] 组件设计文档 (N份) - □ 每份包含 AI 快速理解表 - □ 每份包含双向链接 - □ 每份包含代码入口 (精确到函数) - □ 每份包含架构图 (ASCII Art) - □ 每份包含核心流程说明 - -□ [Type-5] 产品-代码映射文档 - □ 覆盖所有核心 API - □ 每个 API 包含约束表 - □ 每个 API 包含调用链路 - □ 每个 API 包含错误码映射 - -□ [Type-6] 产品规则速查表 - □ 覆盖所有规则类别 - □ 约束值精确 - □ 包含状态互斥矩阵 - -□ [Type-7] 业务开发规范 SOP - □ 包含可运行的代码模板 - □ 包含错误码对照表 - □ 包含 AI 评审 CheckList - -□ [Type-8] 知识增强文档 - □ [8a] 产品知识文库 (概念辨析) - □ [8b] 反模式与踩坑指南 - □ [8c] RPC 接口契约 - □ [8d] 排障案例记录 -``` - -### Phase 3 Checklist: AI-Native 增强 - -``` -□ 所有组件文档包含 AI 快速理解表 -□ 主架构文档包含检索路由规则 -□ 所有文档包含 search-anchor -□ 双向链接网络完整 (0 死链接) -□ QA 对已生成 (10~20 个) -□ 文档优先级已定义 -``` - -### Phase 3b Checklist: 图谱文档集 (Graph RAG) - -``` -□ [G1] 组件依赖关系矩阵 - □ N×N 通信矩阵完整 - □ 正向/反向依赖索引 - □ 外部服务依赖 - -□ [G2] 组件调用链路全景 + 状态机 - □ 核心 API 端到端链路 (读+写) - □ 完整 mermaid 状态机流转图 - □ 核心状态字段值流转路径表(如有内部状态码) - □ 用户可见状态↔内部状态映射关系(如有多层状态) - □ 操作-状态约束速查矩阵 (✅/❌) - □ AI 状态判断推理规则 - -□ [G3] 数据流与存储依赖图 - □ 存储系统依赖矩阵 - □ MQ 队列拓扑 - □ 缓存策略矩阵 - -□ [G4] 错误码组件映射表 - □ 错误码段分配表 - □ 外部→内部错误码映射 - -□ [G5] 跨组件交互场景手册 - □ ≥10 个场景的 mermaid 时序图 - □ 每个场景有异常处理 - -□ [G6] 知识图谱三元组 - □ Ontology 定义 (实体类型+关系类型) - □ 显式三元组 ≥100 条 - □ 多跳依赖路径索引 - □ 反向可达索引 - -□ [G7] 架构风险与影响面分析 - □ 组件风险等级总表 - □ 爆炸半径分析 (≥3 个关键组件) - □ 聚类分析 - □ 变更风险评估矩阵 - -□ [G8] 核心配置参数索引 - □ 分层配置架构图 (mermaid) - □ 每层配置参数表 (配置项/默认值/影响行为/变更风险/生效方式) - □ 配置变更影响面速查矩阵 - -□ [G9] 业务规则约束矩阵 - □ 操作前置条件矩阵 - □ 硬件约束详表 - □ 迁移约束决策树 (mermaid) - □ 计费约束详表 - □ 特殊实例类型约束汇总 (✅/❌/⚠️) - □ AI 推理规则速查 (mermaid 流程图) - -□ 图谱目录 README.md 索引完整 - □ 按问题类型查找表 - □ 检索路由规则建议 -``` - -### Phase 4 Checklist: 质量评估 - -``` -□ 覆盖率 ≥ 90% -□ 代码入口精确度 ≥ 80% -□ 死链接 = 0 (运行 validate_kb.py 确认) -□ RAG 检索准确率 ≥ 85% -□ 核心文档更新滞后 ≤ 30 天 -□ 术语一致性检查通过 -``` diff --git a/skills/team-wiki-codebase/references/templates/project-overview.md b/skills/team-wiki-codebase/references/templates/project-overview.md deleted file mode 100644 index 04dfd19c..00000000 --- a/skills/team-wiki-codebase/references/templates/project-overview.md +++ /dev/null @@ -1,148 +0,0 @@ -# 知识库总览模板 - -> 用于生成 `<output_dir>/README.md`,在 Phase K2 批次5 生成(知识库顶层索引)。 - -```markdown -# <项目名称> — 深度知识库 -<!-- search-anchor: <项目名称>, <项目英文名>, 知识库, 架构总览, 快速导航, 组件文档, Graph RAG, 图谱 --> - -> **AI 读取指引**:本目录是 AI-Native 知识库。请先阅读本文件了解全局和认知边界, -> 再按检索路由规则进入对应文档查阅详情。**禁止一次性读取整个知识库目录。** - -## 🤖 知识库检索路由指引(AI 专用) - -### 按问题类型快速导航 - -| 我想了解… | 应该读… | 路径 | -|---------|---------|------| -| 系统整体架构和分层 | 技术架构文档 | `./{项目名} 技术架构.md` | -| 某个组件的设计和实现 | 组件设计说明 | `./XX_{组件名}设计说明.md` | -| 组件之间的依赖关系 | G1 依赖矩阵 | `./graph/G1_*.md` | -| 某个 API 经过哪些模块 | G2 调用链路全景 | `./graph/G2_*.md` | -| 数据存在哪里、MQ 拓扑 | G3 数据流 | `./graph/G3_*.md` | -| 错误码是哪个模块的 | G4 错误码映射 | `./graph/G4_*.md` | -| 某个业务场景的完整流程 | G5 交互场景手册 | `./graph/G5_*.md` | -| A 间接依赖谁(多跳查询) | G6 知识图谱三元组 | `./graph/G6_*.md` | -| X 组件挂了影响多大 | G7 风险分析 | `./graph/G7_*.md` | -| 怎么修改某个配置 | G8 配置参数索引 | `./graph/G8_*.md` | -| 某个操作能不能执行 | G9 业务规则约束 | `./graph/G9_*.md` | -| 产品约束→代码位置映射 | 核心API映射文档 | `./XX_*产品代码映射.md` | -| 业务开发 SOP | 业务开发规范 | `./XX_*业务开发规范SOP.md` | - -### 检索规则 - -- **规则 1 — 先读索引后深入**:遇到不确定的组件,先读本文件找到正确路径,再深入组件文档 -- **规则 2 — 组件内部问题查组件文档**:核心机制、代码入口、数据模型 → `XX_{组件名}设计说明.md` -- **规则 3 — 跨组件关系问题查图谱**:依赖矩阵、调用链路、影响面 → `graph/` 目录 -- **规则 4 — 操作可行性问题查 G9**:约束矩阵 + 决策树 → `graph/G9_*.md` -- **规则 5 — `[UNVERIFIED]` 标注的内容不可用于代码生成**,需先人工确认 -- **规则 6 — `AMBIGUOUS` 关系不可用于变更影响评估**,需先明确 - ---- - -## 🚧 认知边界声明(AI 必读) - -> 本节声明此知识库**不知道什么**。AI 在回答问题时,如果涉及以下范围, -> **必须主动告知用户"此信息超出知识库覆盖范围,建议查看源代码/产品文档/联系团队"**, -> 而不是尝试推断或幻觉。 - -### 覆盖范围 - -| 维度 | 覆盖 | 说明 | -|------|------|------| -| 代码基准 | `<commit SHA>` (`<tag>`) | 此版本**之后**的变更不在覆盖范围内 | -| 生成时间 | `<YYYY-MM-DDTHH:MM:SSZ>` | 知识库与代码的时间锚点 | -| 核心组件(P0) | <P0组件列表> | 文档深度最高,接口级覆盖 | -| 重要组件(P1) | <P1组件列表> | 文档深度中等,核心机制覆盖 | -| 辅助组件(P2) | <P2组件列表> | 文档深度有限,仅架构层面 | - -### 明确不覆盖(AI 不应尝试回答) - -| 领域 | 原因 | -|------|------| -| 第三方 SDK/库内部实现 | 知识库只记录调用方式,不涉及第三方源码 | -| 运维/部署细节(ansible/k8s 配置) | 超出代码知识库范围,需查阅运维文档 | -| 非代码产出(UI 设计、产品 PRD 原文) | 仅 Type-5/6 桥梁文档有产品约束映射 | -| 历史架构变迁 | 仅反映当前代码基准版本的架构 | -| 性能基准数据 | 知识库不包含压测数据 | -| <项目特定不覆盖项> | <原因> | - -### 低可信度区域(AI 回答时需额外警告) - -| 区域 | 原因 | 建议 | -|------|------|------| -| P2 辅助组件的内部细节 | 文档深度有限 | 引用时加"基于有限文档分析" | -| `[UNVERIFIED]` 标注内容 | 无法回溯到代码 | 必须告知用户"此信息未经代码验证" | -| `AMBIGUOUS` 关系 | 置信度 < 0.3 | 必须告知用户"此关系存在不确定性" | -| 产品文档缺失时的 Type-5/6 | 无产品文档输入 | 标注 `[PRODUCT_DOC_MISSING]` | - -### 知识库更新说明 - -- **增量更新**:使用 `code-to-knowledge --update` 可仅更新变更文件对应的文档 -- **全量重建**:代码发生大规模重构时建议全量重建 -- **上次更新**:`<ISO8601>` - ---- - -## 项目简介 - -<!-- 1-3 句话:项目背景、核心业务目标、主要用户 --> - -## 技术栈 - -| 类别 | 技术 | 说明 | -|------|------|------| -| 语言 | Go / Python | ... | -| 框架 | go-zero / FastAPI | ... | -| 数据库 | MySQL / PostgreSQL | ... | -| 缓存 | Redis | ... | -| 消息队列 | Kafka / RabbitMQ | (如有) | - -## 知识库文档索引 - -### 架构层文档 -| 文档 | 类型 | 规模 | 说明 | -|------|------|------|------| -| {项目名} 技术架构.md | Type-1 | ~200KB | 架构总览 | -| {项目名} 业务架构.md | Type-2 | ~70KB | 产品能力+生命周期 | -| {项目名} 部署架构.md | Type-3 | ~40KB | 部署拓扑 | - -### 组件设计文档 -| 编号 | 组件 | 架构层 | 核心度 | 规模 | -|------|------|--------|--------|------| -| 01 | <组件名> | <层级> | P0 | ~NKB | - -### 桥梁文档(有产品文档时生成) -| 文档 | 类型 | 说明 | -|------|------|------| -| 核心API产品代码映射 | Type-5 | 产品约束→代码位置 | -| 产品规则速查表 | Type-6 | 使用限制/FAQ→代码 | -| 业务开发规范SOP | Type-7 | 开发/变更操作规范 | - -### 图谱文档集(Graph RAG) -| 文档 | 用途 | 规模 | -|------|------|------| -| G1~G9 | 跨组件关系索引 | 详见 `graph/README.md` | - -## 知识库质量概览 - -| 指标 | 数值 | 状态 | -|------|------|------| -| 文档总数 | N 份 | — | -| 内容准确率(有代码引用) | X% | ✅/⚠️ | -| [UNVERIFIED] 比例 | X% | 目标<15% | -| 接口覆盖率(非 NONE 组件) | X% | 目标≥90% | -| AMBIGUOUS 关系数 | N 条 | 需人工确认 | - -> 详细质量报告见 `_review/k4-quality-report.md` - -## 代码基准版本 - -> ⚠️ 本知识库基于以下版本代码生成,代码演进后请运行 `code-to-knowledge --update` 增量更新。 - -- **Commit**:`<git commit SHA>` -- **Tag**:`<tag 或 "无 tag">` -- **生成时间**:`<YYYY-MM-DDTHH:MM:SSZ>` - -> 版本信息来源:`_review/metadata.json` -``` diff --git a/skills/teamai-share-learnings/SKILL.md b/skills/teamai-share-learnings/SKILL.md deleted file mode 100644 index 664bb522..00000000 --- a/skills/teamai-share-learnings/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: teamai-share-learnings -description: "Contribute — 分享 Session 经验到团队知识库" ---- - -# Contribute — 分享 Session 经验到团队知识库 - -总结本次 AI 编码 session 中学到的经验,推送到团队知识库。 - -**【重要】所有生成的文档必须使用中文撰写。** - -## When to Use - -- When teamai suggests this session has valuable content worth sharing -- When you've solved a tricky problem and want to document the solution -- When you've discovered a useful workflow or pattern -- After a long session with diverse tool usage - -## How It Works - -1. **总结**:回顾本次 session 的工具使用、解决的问题、发现的模式 -2. **生成文档**:用中文撰写 Markdown 文档,涵盖: - - 任务/问题是什么 - - 关键决策及原因 - - 解决方案、变通方法或发现的模式 - - 哪些工具/skill 特别有用 - - 踩坑点和注意事项 -3. **保存临时文件**:写入临时文件 -4. **推送到团队**:运行 `teamai contribute --file <path> --title "<title>"` - -## Document Template - -**【必须】文档必须包含 YAML frontmatter,用于搜索索引和知识发现。** - -```markdown ---- -title: "<简短标题,描述核心问题或发现>" -author: <username> -date: <YYYY-MM-DD> -tags: [tag1, tag2, tag3] ---- - -## 背景 -在做什么?遇到了什么问题? - -## 解决方案 -怎么解决的?关键步骤是什么? - -## 经验总结 -- 经验 1 -- 经验 2 - -## 相关 Skills -- skill-name-1 -- skill-name-2 -``` - -### Frontmatter 字段说明 - -| 字段 | 必须 | 说明 | 示例 | -|------|------|------|------| -| title | ✅ | 简短标题(<60 字符) | "K8s Pod OOM 排查指南" | -| author | ✅ | 贡献者用户名 | jeffyxu | -| date | ✅ | 日期 YYYY-MM-DD | 2026-03-28 | -| tags | ✅ | 2-5 个关键标签 | [k8s, oom, troubleshooting] | - -### Tags 选择建议 - -从以下类别中选择 2-5 个: -- **技术栈**: python, typescript, go, k8s, docker, sglang, cuda -- **问题类型**: troubleshooting, performance, deployment, config, api -- **模式**: workflow, pattern, tool-usage, best-practice -- **场景**: debugging, testing, monitoring, security - -## Example - -```bash -# AI 生成总结文档到 /tmp/session-summary.md 后 -teamai contribute --file /tmp/session-summary.md --title "K8s pod 启动超时排查" -``` - -## Important - -- Run this as a **sub-agent** (Agent tool) to avoid polluting the main session's context -- The document is pushed to the team repo's `teamai-learnings` branch, under `learnings/`, with no pull request -- Team members will see it on their next `teamai pull` -- Keep summaries concise and actionable — this is a knowledge base, not a diary diff --git a/skills/teamai/SKILL.md b/skills/teamai/SKILL.md index 7bd1ec87..36fdfc00 100644 --- a/skills/teamai/SKILL.md +++ b/skills/teamai/SKILL.md @@ -1,146 +1,46 @@ --- name: teamai description: >- - Guide for TeamAI — the CLI that syncs a team's AI skills, rules, docs, and env - across AI coding tools (set up, join, manage, contribute, uninstall). Invoke - ONLY when the user explicitly runs `/teamai`. Do NOT auto-trigger from ordinary - conversation, even if words like "team", "skill", or "sync" appear. + Make every team AI native — TeamAI syncs a team's AI skills, rules, docs and env across AI coding + tools. Use when the task operates on team-shared AI configuration or team knowledge: setting up a + team repo, joining one, managing members, syncing with pull or push, or checking team status. + Also use to build or query a codebase knowledge base for a large multi-repo project (architecture + analysis, architecture reverse-engineering, code-to-knowledge, team-wiki-codebase, architecture wiki), + and to share what a session taught you back to the team (share session learnings, contribute a + learning, share what I learned with my team), including + after a friction reminder. Triggers include "set up teamai", "join the team repo", "sync team + skills", "team wiki", "share what I learned", and running /teamai. Talking about a team needs no + skill; operating on what the team shares does. +allowed-tools: Bash(teamai skill:*), Bash(npx teamai-cli skill:*) --- -# TeamAI — Team AI Skills & Rules Sync +# teamai -You are guiding a user through TeamAI. **They may not know Git.** You run the -commands; they only make choices when you ask. Follow the steps literally — -do not skip, reorder, or invent commands. +Make every team AI native — one shared foundation for the skills, rules, docs and env a team works with. -## STEP 0 — Progressive disclosure (do this first, every time) +Install: `npm i -g teamai-cli@latest` (Node.js >= 20). If `teamai skill get` is not recognised, the installed CLI predates it; upgrade the same way. -Look at what the user typed after `/teamai`. +## Start here -**If they gave NO scenario** (bare `/teamai`, or only greetings/no task): -print the menu below **exactly**, then **STOP and wait**. Take no other action — -do not run any command, do not read any reference file yet. +This file is a discovery stub, not the usage guide. Load the workflow from the CLI before running anything, so the instructions match the installed version: +```bash +teamai skill get core # daily work: routing, pull, push, status, doctor +teamai skill get core --full # adds the full command reference and troubleshooting ``` -teamai — Team AI Skills & Rules Sync - -Usage examples (copy one to get started): - - 🏗️ Admin — set up a new team repo: - /teamai Help me set up TeamAI for my team from scratch - - 🤝 Member — join an existing team: - /teamai Help me join my team's TeamAI, repo URL is https://... - 🔧 Admin — daily management (publish & update skills, rules, MCP, env): - /teamai I already have TeamAI set up, help me manage it +The CLI serves skill content that always matches the installed version, so instructions never go stale. The content in this stub cannot change between releases, which is why it just points at `skill get`. - 📊 Anyone — open the team dashboard: - /teamai Open the TeamAI dashboard +## Specialized workflows - 💡 Member — share a skill with the team (just ask in plain language): - /teamai Share this <skill-name> skill with my team - - 🗑️ Anyone — remove TeamAI from this machine: - /teamai Uninstall TeamAI +```bash +teamai skill get setup # day 0: create a team repo (admin) or join one (member), manage, uninstall +teamai skill get wiki # large multi-repo codebase: architecture reverse-engineering and knowledge base +teamai skill get share # turn what this session taught you into a team learning (needs recall on) ``` -**If they DID describe a scenario**, match it to one row of the table below, -then open that reference file and follow it step by step. - -| The user wants to… | Load this reference | -|-----------------------------------------------------|------------------------------------------| -| Set up TeamAI for a team from scratch (create repo) | `references/setup-admin.md` | -| Join their team (with or without a repo URL) | `references/join-member.md` | -| Manage a team: publish/update skills, rules, MCP, env, invite members | `references/manage-admin.md` | -| Share / publish a skill with the team ("share this xxx skill") — any member, not just admins | `references/contribute-member.md` | -| Open the team dashboard (web UI) | run `teamai dashboard` (see cheat sheet) | -| Remove / uninstall TeamAI from this machine | `references/uninstall.md` | - -> **Sharing session *learnings* is automatic — not a menu choice, and not routed -> here.** TeamAI prompts on its own at the end of a session worth sharing, and the -> separate **`teamai-share-learnings`** skill summarizes it and runs -> `teamai contribute`. The user never asks for it through `/teamai`. (Only when the -> admin left team sharing on — the default.) The `contribute-member.md` row above is -> a *different* task: a member **publishing a reusable skill** on request ("share -> this xxx skill with my team"). +Publishing a skill, rule or doc the user already has is in `core`; it needs no recall. -Choosing between "set up" and "join": a user **setting up a new team** becomes its -admin and creates the repo; a user **joining an existing team** needs a repo URL -from their admin. If someone wants to join but has no URL, that is still the -**join** flow — `join-member.md` tells them to ask their admin for it. Do **not** -send a would-be member to the setup/create-repo flow just because they lack a URL. - -If the request is ambiguous (e.g. "help me with teamai" with no direction), -ask ONE short question to pick a row, then proceed. When something breaks at any -step, load `references/troubleshooting.md`. - -## Global rules (apply to every scenario) - -1. **Reply in the user's language — including every example and hand-off blurb.** - Answer in whatever language the user used to invoke the skill (Chinese in → - Chinese out, English in → English out, and so on), for the whole conversation. - This applies to **everything you write**, not just prose: the reference files - below are written in English, but any ready-made sentence they hand you — the - invite line you give an admin to forward to members, the one-line explanations, - the "what's next" summary — **must be translated into the user's language before - you show it.** Do not paste an English example at a Chinese-speaking user. - *Only* commands, flags, URLs, file paths, and code identifiers stay verbatim - (never translate `teamai pull`, `--scope user`, `/teamai`, a repo URL, etc.). - Example: for a Chinese user, the member-invite line becomes - `/teamai 帮我加入团队的 TeamAI,仓库地址是 https://...`, not the English form. -2. **Never teach Git.** Do not mention branches, commits, clone, or push/pull of - Git itself. TeamAI hides all of that. The user thinks in terms of "my team's - skills", not repositories. -3. **Always use a full URL** for the team repo (e.g. - `https://github.com/yourorg/yourrepo`). Never use the `owner/repo` short form. -4. **You run the commands.** Only pause to ask the user when you need a web login, - a value only they know, or a genuine either/or choice. Show each command before - you run it, in one short line. -5. **Detect the current AI tool first.** TeamAI behaves differently per host. Note - which tool this conversation is running in (Claude Code, Cursor, CodeBuddy, - WorkBuddy, ChatGPT App, Codex, OpenCode, Kiro, Gemini CLI, …). When you reopen a - session, use the name of **this** tool — do not assume Claude Code or Cursor. - Some hosts need extra manual steps for hooks — see - `references/troubleshooting.md` ("Agent-specific caveats"). -6. **Prerequisite:** Node.js ≥ 20. Install once with `npm install -g teamai-cli` - and verify with `teamai --version`. -7. **Finish with `teamai doctor`.** Every setup/onboarding flow ends by running - `teamai doctor` and resolving whatever it reports before you call it done. -8. **After init, resources appear on the NEXT session.** `teamai init` injects a - session-start hook that auto-runs `teamai pull`. It is normal that the skills/ - rules directories are empty right after init — they fill in when the user opens - a fresh session in this tool. To sync immediately, run `teamai pull`. -9. **Don't limit which AI tools get set up — cover all of them by default.** Unless - the user explicitly says "only install to Claude Code" (or names specific - tools), do **not** pass `--agent` to restrict the install. Let `teamai init` set - up **every AI tool already installed on the machine** (omitting `--agent` gives - an interactive picker; select all detected tools, or the user's stated subset). - **After init, report which agents were set up** — tell the user, in their - language, exactly which tools will now auto-start TeamAI (and which detected - tools were skipped and why, e.g. Codex trust-gate / CodeBuddy design). Verify - the real per-tool result with `teamai doctor` / `teamai hooks list`. - -## Command cheat sheet (ground truth — do not invent flags) - -```bash -teamai init <full-repo-url> # Set up / join a team (configure provider, clone, register) -teamai init <url> --scope user # Install for the whole machine instead of just this project -teamai pull # Sync team resources into local AI tools now -teamai push # Publish your local skills/rules/docs to the team -teamai doctor # Diagnose configuration and hook problems -teamai status # Show local vs team differences -teamai list # List resources (skills|rules|docs|env|agents|hooks|mcp) -teamai members # See team members (subcommand: teamai members list) -teamai roles # Manage roles / resource namespaces -teamai projects # Manage multiple projects from one repo (list|set|members) -teamai packages # Install team-declared npm packages & Claude plugins -teamai env # Manage shared team environment variables -teamai dashboard # Open the AI coding session dashboard (web UI, default port 3721) -teamai contribute --file <p> --title <t> # Contribute a knowledge doc (usually via the teamai-share-learnings skill) -``` +A friction reminder at the end of a turn means `teamai skill get share`. -Anything not in this cheat sheet: check `teamai <command> --help` before using it. -Do **not** guess flags (for example, there is no member-invite flag in the CLI — -inviting a member is done on the Git platform's website; see -`references/manage-admin.md`). +`teamai skill list` shows everything the installed version serves. `teamai skill path <name>` prints the directory holding a skill's scripts and templates. diff --git a/src/__tests__/agent-skills.test.ts b/src/__tests__/agent-skills.test.ts index baef666a..4c79c2b4 100644 --- a/src/__tests__/agent-skills.test.ts +++ b/src/__tests__/agent-skills.test.ts @@ -174,6 +174,13 @@ describe('classifySkill', () => { expect(formatSkillSource(cls)).toBe('[source:partner]'); }); + it('returns [builtin] for the names a pre-stub release deployed, until pull prunes them', async () => { + const ctx = await buildClassifyContext(fx.localConfig); + for (const legacy of ['team-wiki-codebase', 'teamai-share-learnings']) { + expect(classifySkill(legacy, ctx).kind).toBe('builtin'); + } + }); + it('returns [local-only] when skill is unknown to repo, sources and builtins', async () => { const ctx = await buildClassifyContext(fx.localConfig); const cls = classifySkill('only-local', ctx); diff --git a/src/__tests__/codex-stop-hint.test.ts b/src/__tests__/codex-stop-hint.test.ts index f2aa4a89..da14ca64 100644 --- a/src/__tests__/codex-stop-hint.test.ts +++ b/src/__tests__/codex-stop-hint.test.ts @@ -39,7 +39,7 @@ describe('Codex Stop hint handoff with persisted session state', () => { expect(await stop.execute(stdin, 'codex')).toBeNull(); const state = await readContributeState(stdin.session_id); expect(state.hinted).toBe(true); - expect(state.pendingHint).toContain('teamai-share-learnings'); + expect(state.pendingHint).toContain('teamai skill get share'); expect(await stop.execute(stdin, 'codex')).toBeNull(); expect((await readContributeState(stdin.session_id)).pendingHint).toBe(state.pendingHint); expect(JSON.parse((await prompt.execute(stdin, 'codex'))!)).toEqual({ diff --git a/src/__tests__/commands-reference.test.ts b/src/__tests__/commands-reference.test.ts new file mode 100644 index 00000000..4485bd79 --- /dev/null +++ b/src/__tests__/commands-reference.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect, vi } from 'vitest'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { renderCommandsReference, COMMANDS_REFERENCE_PATH } from '../commands-reference.js'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + +describe('generated command reference', () => { + it('matches the CLI command table', async () => { + // Guard the CLI entry so importing it yields the command table instead of + // parsing this test run's argv. + vi.stubEnv('TEAMAI_COMMAND_TABLE_ONLY', '1'); + const { program } = await import('../index.js'); + + // Regenerate with `npx vitest run commands-reference -u` when a command, + // subcommand or flag changes — the skill must not document a CLI that no + // longer exists. + await expect(renderCommandsReference(program)).toMatchFileSnapshot( + path.join(ROOT, COMMANDS_REFERENCE_PATH), + ); + }); +}); diff --git a/src/__tests__/config-not-initialized.test.ts b/src/__tests__/config-not-initialized.test.ts new file mode 100644 index 00000000..6de26fb9 --- /dev/null +++ b/src/__tests__/config-not-initialized.test.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs, { realpathSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +vi.mock('../utils/logger.js', () => ({ + log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), dim: vi.fn() }, + setStderrOnly: vi.fn(() => false), +})); + +import { NotInitializedError, detectProjectConfig, findUnreadableProjectConfig, requireInit } from '../config.js'; +import { projectDataHome } from '../utils/partition.js'; + +/** + * `loadLocalConfig` returns null both for a missing file and for one it could + * not use. Commands that work without a team fall back on NotInitializedError + * alone, so only the missing file may produce it. + */ +describe('requireInit: missing config versus unreadable config', () => { + let home: string; + + beforeEach(() => { + home = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-config-init-')); + vi.stubEnv('HOME', home); + vi.stubEnv('USERPROFILE', home); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + fs.rmSync(home, { recursive: true, force: true }); + }); + + it('is NotInitializedError when there is no config file', async () => { + await expect(requireInit()).rejects.toBeInstanceOf(NotInitializedError); + }); + + it('names the file, and is not NotInitializedError, when the config exists but does not parse', async () => { + const configPath = path.join(home, '.teamai', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, 'repo: [unclosed\n'); + + const error = await requireInit().catch((e: unknown) => e); + expect(error).toBeInstanceOf(Error); + expect(error).not.toBeInstanceOf(NotInitializedError); + expect(String(error)).toContain(configPath); + }); + + it('is not NotInitializedError when the config parses but fails validation', async () => { + const configPath = path.join(home, '.teamai', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, 'username: 42\n'); + + await expect(requireInit()).rejects.not.toBeInstanceOf(NotInitializedError); + }); +}); + +describe('findUnreadableProjectConfig', () => { + let dir: string; + + beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-project-config-')); + }); + + afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }); + }); + + it('names a project config that exists but does not parse, which detection alone skips', async () => { + const configPath = path.join(dir, '.teamai', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, 'repo: [unclosed\n'); + + expect(await findUnreadableProjectConfig(dir)).toContain(configPath); + }); + + it('is null when there is no project config at all', async () => { + expect(await findUnreadableProjectConfig(dir)).toBeNull(); + }); + + it('names an empty project config, which detection alone also skips', async () => { + const configPath = path.join(dir, '.teamai', 'config.yaml'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, ''); + + expect(await findUnreadableProjectConfig(dir)).toContain(configPath); + }); + + it('names a broken partition config even when the legacy .teamai/ config behind it loads', async () => { + // The partition is authoritative; detection skips it when broken and lands + // on the legacy config, which may belong to another team. + const home = path.join(dir, 'home'); + fs.mkdirSync(home); + vi.stubEnv('HOME', home); + vi.stubEnv('USERPROFILE', home); + try { + const repo = path.join(dir, 'repo'); + fs.mkdirSync(repo); + for (const args of [['init', '-q'], ['config', 'user.email', 't@e'], ['config', 'user.name', 'T'], ['commit', '--allow-empty', '-q', '-m', 'init']]) { + execFileSync('git', args, { cwd: repo, stdio: 'pipe' }); + } + const anchor = realpathSync(repo); + const partitionConfig = path.join(projectDataHome(anchor), 'config.yaml'); + fs.mkdirSync(path.dirname(partitionConfig), { recursive: true }); + fs.writeFileSync(partitionConfig, 'repo: [unclosed\n'); + fs.mkdirSync(path.join(repo, '.teamai')); + fs.writeFileSync(path.join(repo, '.teamai', 'config.yaml'), + `repo:\n localPath: ${path.join(repo, '.teamai', 'team-repo')}\n remote: https://example.com/other.git\nusername: t\nscope: project\n`); + + expect(await detectProjectConfig(repo)).not.toBeNull(); + expect(await findUnreadableProjectConfig(repo)).toContain(partitionConfig); + } finally { + vi.unstubAllEnvs(); + } + }); +}); + diff --git a/src/__tests__/contribute-check-e2e.test.ts b/src/__tests__/contribute-check-e2e.test.ts index 94e0e8b5..9b1261ba 100644 --- a/src/__tests__/contribute-check-e2e.test.ts +++ b/src/__tests__/contribute-check-e2e.test.ts @@ -162,7 +162,7 @@ describe('contribute-check E2E', () => { expect(result.stdout).toBe(''); const state = readSessionState(tmpHome, SESSION_ID)!; expect(state.hinted).toBe(true); - expect(state.pendingHint).toContain('teamai-share-learnings'); + expect(state.pendingHint).toContain('teamai skill get share'); const repeated = await runContributeCheck(tmpHome, makeStdinPayload(SESSION_ID), 'codex'); expect(repeated.stdout).toBe(''); expect(readSessionState(tmpHome, SESSION_ID)!.pendingHint).toBe(state.pendingHint); @@ -194,7 +194,7 @@ describe('contribute-check E2E', () => { expect(parsed.hookSpecificOutput.additionalContext).not.toContain(RAW_GITHUB_TOKEN); expect(parsed.hookSpecificOutput.additionalContext).not.toContain('50 tool calls'); expect(parsed.hookSpecificOutput.additionalContext).not.toContain('7 different tools'); - expect(parsed.hookSpecificOutput.additionalContext).toContain('/teamai-share-learnings'); + expect(parsed.hookSpecificOutput.additionalContext).toContain('/teamai'); expect(parsed.stopReason).toBeUndefined(); // The real CLI persists hinted=true, so a repeated Stop hook is silent. diff --git a/src/__tests__/e2e/codebase-extract-cli.test.ts b/src/__tests__/e2e/codebase-extract-cli.test.ts index 5e531b90..de66be38 100644 --- a/src/__tests__/e2e/codebase-extract-cli.test.ts +++ b/src/__tests__/e2e/codebase-extract-cli.test.ts @@ -82,7 +82,7 @@ describe('teamai codebase extract CLI (issue #360 slice 1)', () => { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-local-workflow-')); const caller = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-workflow-caller-')); try { - const skill = fs.readFileSync(path.join(ROOT, 'skills/team-wiki-codebase/SKILL.md'), 'utf8'); + const skill = fs.readFileSync(path.join(ROOT, 'skill-data/wiki/SKILL.md'), 'utf8'); const commands = [...skill.matchAll(/`(teamai codebase [^`]+)`/g)].map(match => match[1]); const refresh = commands.find(command => command.includes('--incremental')); const lint = commands.find(command => command.includes('--lint')); @@ -147,7 +147,7 @@ describe('teamai codebase reconcile CLI (issue #360 slice 2)', () => { const help = await runCLI(['codebase', '--help']); expect(help.code, help.output).toBe(0); expect(help.stdout).toContain('--reconcile'); - const skill = fs.readFileSync(path.join(ROOT, 'skills/team-wiki-codebase/SKILL.md'), 'utf8'); + const skill = fs.readFileSync(path.join(ROOT, 'skill-data/wiki/SKILL.md'), 'utf8'); const command = [...skill.matchAll(/`(teamai codebase [^`]+)`/g)] .map(match => match[1]) .find(candidate => candidate.includes('--reconcile')); @@ -294,7 +294,7 @@ describe('teamai codebase deep-enrich CLI (issue #360 slice 3)', () => { expect(help.code, help.output).toBe(0); expect(help.stdout).toContain('--deep-enrich'); - const skill = fs.readFileSync(path.join(ROOT, 'skills/team-wiki-codebase/SKILL.md'), 'utf8'); + const skill = fs.readFileSync(path.join(ROOT, 'skill-data/wiki/SKILL.md'), 'utf8'); const command = [...skill.matchAll(/`(teamai codebase [^`]+)`/g)] .map(match => match[1]) .find(candidate => candidate.includes('--deep-enrich')); diff --git a/src/__tests__/e2e/enabled-agents-whitelist.test.ts b/src/__tests__/e2e/enabled-agents-whitelist.test.ts index 622dc445..652b6f27 100644 --- a/src/__tests__/e2e/enabled-agents-whitelist.test.ts +++ b/src/__tests__/e2e/enabled-agents-whitelist.test.ts @@ -132,10 +132,10 @@ describe('enabledAgents whitelist on real CLI pull (#510)', () => { expect(first.output).not.toContain('Already synced'); expect(fs.existsSync(path.join(homeDir, '.workbuddy', 'skills', 'team-skill', 'SKILL.md'))).toBe(true); - expect(fs.existsSync(path.join(homeDir, '.workbuddy', 'skills', 'team-wiki-codebase', 'SKILL.md'))).toBe(true); + expect(fs.existsSync(path.join(homeDir, '.workbuddy', 'skills', 'teamai', 'SKILL.md'))).toBe(true); expect(fs.existsSync(path.join(homeDir, '.hermes', 'skills', 'team-skill'))).toBe(false); - expect(fs.existsSync(path.join(homeDir, '.hermes', 'skills', 'team-wiki-codebase'))).toBe(false); + expect(fs.existsSync(path.join(homeDir, '.hermes', 'skills', 'teamai'))).toBe(false); expect(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'team-skill'))).toBe(false); expect(fs.existsSync(path.join(homeDir, '.codebuddy', 'skills', 'team-skill'))).toBe(false); diff --git a/src/__tests__/e2e/skill-serving-cli.test.ts b/src/__tests__/e2e/skill-serving-cli.test.ts new file mode 100644 index 00000000..2b6688e6 --- /dev/null +++ b/src/__tests__/e2e/skill-serving-cli.test.ts @@ -0,0 +1,246 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); +const CLI = path.join(ROOT, 'dist', 'index.js'); + +/** + * The built CLI, run the way an agent runs it: through a shell, reading stdout. + * The unit tests call the functions; this proves the packaged binary resolves + * its own content and keeps stdout clean. + */ +describe('teamai skill get / path CLI (e2e)', () => { + let home: string; + + function run(...args: string[]) { + return spawnSync(process.execPath, [CLI, ...args], { + cwd: home, + env: { ...process.env, HOME: home, USERPROFILE: home, FORCE_COLOR: '0' }, + encoding: 'utf8', + }); + } + + beforeAll(() => { + home = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-skill-serving-e2e-')); + }); + + afterAll(() => { + fs.rmSync(home, { recursive: true, force: true }); + }); + + it('serves every skill it lists', () => { + const listed = run('skill', 'list', '--json'); + expect(listed.status).toBe(0); + + const catalog = JSON.parse(listed.stdout) as { skills: Array<{ name: string; path: string }> }; + expect(catalog.skills.map((s) => s.name)).toEqual(['core', 'setup', 'share', 'wiki']); + + for (const skill of catalog.skills) { + const got = run('skill', 'get', skill.name); + expect(got.status, skill.name).toBe(0); + expect(got.stderr, skill.name).toBe(''); + // Byte-identical to the packaged file, bar the resolved placeholder. + const raw = fs.readFileSync(path.join(skill.path, 'SKILL.md'), 'utf8'); + expect(got.stdout, skill.name).toBe(raw.split('{SKILL_DIR}').join(skill.path)); + expect(got.stdout, skill.name).not.toContain('{SKILL_DIR}'); + } + }); + + it('serves every skill with --all and no name, and fails with neither', () => { + const all = run('skill', 'get', '--all'); + expect(all.status, all.stderr).toBe(0); + // No team config in this HOME, so the recall gate fails open and all four are served. + expect(all.stdout.match(/^name: /gm)).toHaveLength(4); + + const none = run('skill', 'get'); + expect(none.status).toBe(1); + expect(none.stdout).toBe(''); + expect(none.stderr).toContain('No skill name provided'); + }); + + it('refuses skill path without a name, so the skill-data root is never printed', () => { + const bare = run('skill', 'path'); + expect(bare.status).toBe(1); + expect(bare.stdout).toBe(''); + expect(bare.stderr).toContain("missing required argument 'name'"); + }); + + it('runs the wiki scripts from the directory it prints', () => { + const printed = run('skill', 'path', 'wiki'); + expect(printed.status).toBe(0); + + const dir = printed.stdout.trim(); + for (const script of ['scan_repo.py', 'validate_kb.py']) { + const scriptPath = path.join(dir, 'scripts', script); + expect(fs.existsSync(scriptPath), scriptPath).toBe(true); + + const help = spawnSync('python3', [scriptPath, '--help'], { + encoding: 'utf8', + // Do not leave bytecode in the packaged tree the test just proved ships. + env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' }, + }); + // A machine without python3 cannot run them; the path is what we assert there. + if (help.error) continue; + expect(help.status, script).toBe(0); + expect(help.stdout, script).toContain('usage:'); + } + }); + + it('keeps content on stdout and diagnostics on stderr', () => { + const unknown = run('skill', 'get', 'no-such-skill'); + expect(unknown.status).toBe(1); + expect(unknown.stdout).toBe(''); + expect(unknown.stderr).toContain('Skill not found: no-such-skill'); + + const hallucinatedFlag = run('skill', 'get', 'core', '--not-a-flag'); + expect(hallucinatedFlag.status).toBe(0); + expect(hallucinatedFlag.stderr).toContain('Unknown flag ignored: --not-a-flag'); + expect(hallucinatedFlag.stdout).toContain('name: core'); + + const legacyName = run('skill', 'get', 'team-wiki-codebase'); + expect(legacyName.status).toBe(0); + expect(legacyName.stdout).toContain('name: wiki'); + + // Before `teamai init`, an unknown name is a not-found line, not the stack + // trace of the init error the team lookup would have thrown. + // `skill show` is a human command: the error is on stderr and the way out + // is a dim line on stdout, as on the initialised not-found path. + const shownUnknown = run('skill', 'show', 'no-such-skill'); + expect(shownUnknown.status).toBe(1); + expect(shownUnknown.stderr).toContain('not found among the skills the installed CLI serves'); + expect(shownUnknown.stdout).toContain('Run `teamai init` first'); + expect(shownUnknown.stdout + shownUnknown.stderr).not.toContain(' at '); + }); + + it('reports an unreadable config instead of calling the machine uninitialized', () => { + // A config that exists but does not parse is not "no team": the packaged + // fallback and its `teamai init` hint are for a machine with no config. + const brokenHome = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-skill-broken-config-')); + try { + fs.mkdirSync(path.join(brokenHome, '.teamai'), { recursive: true }); + fs.writeFileSync(path.join(brokenHome, '.teamai', 'config.yaml'), 'repo: [unclosed\n'); + for (const args of [['skill', 'list'], ['skill', 'show', 'core']]) { + const result = spawnSync(process.execPath, [CLI, ...args], { + cwd: brokenHome, + env: { ...process.env, HOME: brokenHome, USERPROFILE: brokenHome, FORCE_COLOR: '0' }, + encoding: 'utf8', + }); + expect(result.status, args.join(' ')).not.toBe(0); + expect(result.stderr, args.join(' ')).toContain('could not be read'); + expect(result.stdout + result.stderr, args.join(' ')).not.toContain('Not initialized'); + expect(result.stdout + result.stderr, args.join(' ')).not.toContain('No team is set up'); + } + } finally { + fs.rmSync(brokenHome, { recursive: true, force: true }); + } + }); + + it('appends the nested references with --full', () => { + const full = run('skill', 'get', 'wiki', '--full'); + expect(full.status).toBe(0); + + const separators = full.stdout.split('\n').filter((line) => line.startsWith('--- ')); + expect(separators).toContain('--- references/methodology/phase0-collection.md ---'); + expect(separators).toContain('--- references/phases/phase0-init.md ---'); + // Sorted by relative path, references before templates. + expect([...separators].sort()).toEqual(separators); + expect(full.stdout.length).toBeGreaterThan(run('skill', 'get', 'wiki').stdout.length); + }); +}); + +/** + * The gate the deployment restriction became. The HOME above has no team + * config, so every call there fails open; this one carries a team whose recall + * is off (the default for a fresh team), the case a member actually hits. + */ +describe('teamai skill recall gate CLI (e2e)', () => { + let home: string; + + function run(...args: string[]) { + return spawnSync(process.execPath, [CLI, ...args], { + cwd: home, + env: { ...process.env, HOME: home, USERPROFILE: home, FORCE_COLOR: '0' }, + encoding: 'utf8', + }); + } + + beforeAll(() => { + home = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-skill-recall-e2e-')); + const repo = path.join(home, '.teamai', 'team-repo'); + fs.mkdirSync(repo, { recursive: true }); + fs.writeFileSync(path.join(repo, 'teamai.yaml'), [ + 'team: recall-gate-e2e', + `repo: ${repo}`, + 'provider: git', + 'usageReport: false', + 'sharing:', + ' env:', + ' injectShellProfile: false', + ].join('\n')); + fs.writeFileSync(path.join(home, '.teamai', 'config.yaml'), [ + 'repo:', + ` localPath: ${repo}`, + ` remote: ${repo}`, + 'username: e2e-user', + 'updatePolicy: skip', + 'scope: user', + ].join('\n')); + }); + + afterAll(() => { + fs.rmSync(home, { recursive: true, force: true }); + }); + + it('withholds share on every content path while recall is off, and serves it once enabled', () => { + const status = run('recall', 'status'); + expect(status.stdout, status.stderr).toContain('Recall: disabled'); + + const byName = run('skill', 'get', 'share'); + expect(byName.status).toBe(1); + expect(byName.stdout).toBe(''); + expect(byName.stderr).toContain('share needs recall'); + expect(byName.stderr).toContain('teamai recall enable'); + + const all = run('skill', 'get', '--all'); + expect(all.status, all.stderr).toBe(0); + expect(all.stdout.match(/^name: /gm)).toEqual(['name: ', 'name: ', 'name: ']); + expect(all.stdout).not.toContain('name: share'); + expect(all.stderr).toContain('Skipped share'); + + const dir = run('skill', 'path', 'share'); + expect(dir.status).toBe(1); + expect(dir.stdout).toBe(''); + expect(dir.stderr).toContain('share needs recall'); + + const shown = run('skill', 'show', 'share'); + expect(shown.status).toBe(1); + expect(shown.stdout).not.toContain('skill: share'); + expect(shown.stdout).not.toContain('skill-data'); + + const core = run('skill', 'show', 'core'); + expect(core.status, core.stderr).toBe(0); + expect(core.stdout).toContain('Source : [builtin]'); + expect(core.stdout).toContain('Read it with : teamai skill get core'); + + const listed = run('skill', 'list', '--json'); + expect(listed.status).toBe(0); + const catalog = JSON.parse(listed.stdout) as { skills: Array<{ name: string; path: string | null; blockedBy: string | null }> }; + expect(catalog.skills.find((s) => s.name === 'share')).toMatchObject({ blockedBy: 'recall', path: null }); + expect(catalog.skills.filter((s) => s.name !== 'share').every((s) => s.blockedBy === null && s.path !== null)).toBe(true); + + const enable = run('recall', 'enable'); + expect(enable.status, enable.stderr).toBe(0); + + expect(run('skill', 'get', 'share').stdout).toContain('name: share'); + expect(run('skill', 'get', '--all').stdout.match(/^name: /gm)).toHaveLength(4); + const servedDir = run('skill', 'path', 'share').stdout.trim(); + expect(fs.existsSync(path.join(servedDir, 'SKILL.md'))).toBe(true); + expect(run('skill', 'show', 'share').stdout).toContain(`Package dir : ${servedDir}/`); + const after = JSON.parse(run('skill', 'list', '--json').stdout) as { skills: Array<{ name: string; path: string | null; blockedBy: string | null }> }; + expect(after.skills.find((s) => s.name === 'share')).toMatchObject({ blockedBy: null, path: servedDir }); + }); +}); diff --git a/src/__tests__/helpers/shipped-skills.ts b/src/__tests__/helpers/shipped-skills.ts new file mode 100644 index 00000000..fa73c704 --- /dev/null +++ b/src/__tests__/helpers/shipped-skills.ts @@ -0,0 +1,51 @@ +import { createHash } from 'node:crypto'; + +/** + * Stand-in content for "a file a release shipped", for tests that exercise the + * legacy prune without the real historical blobs. Pair it with + * `shippedSkillDigestsMock()` in a `vi.mock('../packaged-skill-digests.js')`: + * a file holding `shipped(skill, path)` is the CLI's, anything else at the same + * path is the member's. + */ +export function shipped(skill: string, relative: string, release: 1 | 2 = 1): string { + return `# shipped ${skill}/${relative} (release ${release})\n`; +} + +const PATHS: Readonly<Record<string, readonly string[]>> = { + teamai: [ + 'SKILL.md', + 'references/contribute-member.md', + 'references/join-member.md', + 'references/manage-admin.md', + 'references/provider-tgit.md', + 'references/setup-admin.md', + 'references/troubleshooting.md', + 'references/uninstall.md', + ], + 'teamai-share-learnings': ['SKILL.md'], + 'team-wiki-codebase': [ + 'SKILL.md', + 'README.md', + 'references/agents/graph-rag-agent.md', + 'references/agents/kb-doc-generator.md', + 'references/methodology/phase0-collection.md', + 'references/methodology/phase1-reverse-engineering.md', + 'references/methodology/phase2-document-types.md', + 'references/methodology/phase3-ai-enhancement.md', + 'references/methodology/phase4-quality.md', + 'references/templates/project-overview.md', + 'scripts/scan_repo.py', + 'scripts/validate_kb.py', + ], +}; + +/** The module shape of `packaged-skill-digests.ts`: two shipped releases of every path. */ +export function shippedSkillDigestsMock(): { PACKAGED_SKILL_DIGESTS: ReadonlyMap<string, ReadonlyMap<string, readonly string[]>> } { + const sha = (text: string): string => createHash('sha256').update(text).digest('hex'); + return { + PACKAGED_SKILL_DIGESTS: new Map(Object.entries(PATHS).map(([skill, paths]) => [ + skill, + new Map(paths.map((relative) => [relative, [sha(shipped(skill, relative, 1)), sha(shipped(skill, relative, 2))]])), + ])), + }; +} diff --git a/src/__tests__/hook-handlers.test.ts b/src/__tests__/hook-handlers.test.ts index a6a48bf9..a0f12ab8 100644 --- a/src/__tests__/hook-handlers.test.ts +++ b/src/__tests__/hook-handlers.test.ts @@ -77,7 +77,9 @@ vi.mock('../update.js', () => ({ const mockAutoDetectInit = vi.fn().mockResolvedValue({ localConfig: { repo: { localPath: '/tmp', remote: '' }, username: 'test', scope: 'user' }, - teamConfig: { team: 'test', repo: '', toolPaths: {} }, + // Recall on: the contribute hint routes to the share workflow, which is + // refused while recall is off, so the hint is withheld there too. + teamConfig: { team: 'test', repo: '', toolPaths: {}, sharing: { recall: { enabled: true } } }, }); vi.mock('../config.js', async (importOriginal) => ({ @@ -369,7 +371,7 @@ describe('hook-handlers registry', () => { )!.handler; mockAutoDetectInit.mockResolvedValueOnce({ localConfig: { repo: { localPath: '/tmp', remote: '' }, username: 'test', scope: 'user', contributeHintEnabled: true }, - teamConfig: { team: 'test', repo: '', toolPaths: {}, sharing: { contributeHint: { enabled: false } } }, + teamConfig: { team: 'test', repo: '', toolPaths: {}, sharing: { contributeHint: { enabled: false }, recall: { enabled: true } } }, }); mockContributeCheckForSession.mockResolvedValueOnce({ hint: '[teamai] do share' }); @@ -377,18 +379,65 @@ describe('hook-handlers registry', () => { expect(result).toContain('do share'); }); - it('contribute-check handler keeps hinting when config cannot be loaded', async () => { + it('contribute-check handler stays silent while recall is off, since `teamai skill get share` would refuse', async () => { const registry = buildHandlerRegistry(); const handler = registry.find( (r) => r.event === 'stop' && r.handler.name === 'contribute-check', )!.handler; - mockAutoDetectInit.mockRejectedValueOnce(new Error('not initialized')); + mockAutoDetectInit.mockResolvedValueOnce({ + localConfig: { repo: { localPath: '/tmp', remote: '' }, username: 'test', scope: 'user' }, + teamConfig: { team: 'test', repo: '', toolPaths: {} }, + }); + mockContributeCheckForSession.mockClear(); + + const result = await handler.execute({ session_id: 's3b', cwd: '/x' }, 'claude'); + expect(result).toBeNull(); + expect(mockContributeCheckForSession).not.toHaveBeenCalled(); + }); + + it('contribute-check handler stays silent on a read-only HTTP source even with recall on', async () => { + const registry = buildHandlerRegistry(); + const handler = registry.find( + (r) => r.event === 'stop' && r.handler.name === 'contribute-check', + )!.handler; + mockAutoDetectInit.mockResolvedValueOnce({ + localConfig: { repo: { kind: 'http', localPath: '/tmp', remote: '' }, username: 'test', scope: 'user' }, + teamConfig: { team: 'test', repo: '', toolPaths: {}, sharing: { recall: { enabled: true } } }, + }); + mockContributeCheckForSession.mockClear(); + + const result = await handler.execute({ session_id: 's3c', cwd: '/x' }, 'claude'); + expect(result).toBeNull(); + expect(mockContributeCheckForSession).not.toHaveBeenCalled(); + }); + + it('contribute-check handler keeps hinting when there is no config at all', async () => { + const { NotInitializedError } = await import('../config.js'); + const registry = buildHandlerRegistry(); + const handler = registry.find( + (r) => r.event === 'stop' && r.handler.name === 'contribute-check', + )!.handler; + mockAutoDetectInit.mockRejectedValueOnce(new NotInitializedError('teamai is not initialized. Run `teamai init` first.')); mockContributeCheckForSession.mockResolvedValueOnce({ hint: '[teamai] do share' }); const result = await handler.execute({ session_id: 's5', cwd: '/x' }, 'claude'); expect(result).toContain('do share'); }); + it('contribute-check handler stays silent when a config exists but cannot be loaded', async () => { + const registry = buildHandlerRegistry(); + const handler = registry.find( + (r) => r.event === 'stop' && r.handler.name === 'contribute-check', + )!.handler; + // `teamai skill get share` refuses on such a config, so the nudge would lead nowhere. + mockAutoDetectInit.mockRejectedValueOnce(new Error('Team config (teamai.yaml) not found. Check your repo path.')); + mockContributeCheckForSession.mockClear(); + + const result = await handler.execute({ session_id: 's5b', cwd: '/x' }, 'claude'); + expect(result).toBeNull(); + expect(mockContributeCheckForSession).not.toHaveBeenCalled(); + }); + it('contribute-check handler obeys TEAMAI_CONTRIBUTE_HINT_DISABLED=1', async () => { const registry = buildHandlerRegistry(); const handler = registry.find( diff --git a/src/__tests__/init.test.ts b/src/__tests__/init.test.ts index 12cc057b..77719dc7 100644 --- a/src/__tests__/init.test.ts +++ b/src/__tests__/init.test.ts @@ -561,7 +561,7 @@ describe('init', () => { }); describe('deploys built-in skills after init', () => { - it('calls deployBuiltinSkills with teamConfig and skipRecall when loadTeamConfig returns non-null', async () => { + it('calls deployBuiltinSkills with teamConfig when loadTeamConfig returns non-null', async () => { let cloneDone = false; pathExistsFn = (p: string) => { if (p === localPath) return cloneDone; @@ -592,10 +592,11 @@ describe('init', () => { await init({ repo: 'https://git.woa.com/HyperAI/teamai-test.git', scope: 'user' }); expect(mockDeployBuiltinSkills).toHaveBeenCalled(); + // No recall option: one stub deploys for everyone, and `teamai skill get + // share` is where recall is checked (#678). expect(mockDeployBuiltinSkills).toHaveBeenCalledWith( expect.objectContaining({ team: expect.any(String) }), expect.anything(), - expect.objectContaining({ skipRecall: expect.any(Boolean) }), ); }); }); diff --git a/src/__tests__/pull-skip-sync.test.ts b/src/__tests__/pull-skip-sync.test.ts index 90f2de22..bbb7e235 100644 --- a/src/__tests__/pull-skip-sync.test.ts +++ b/src/__tests__/pull-skip-sync.test.ts @@ -1178,8 +1178,8 @@ describe('enabledAgents whitelist on pull inject, skip-sync, and cleanup (#510)' expect(log.success).toHaveBeenCalledWith( expect.stringContaining('Already synced at abc1234, skipping'), ); - expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/team-wiki-codebase/SKILL.md'))).toBe(true); - expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/team-wiki-codebase'))).toBe(false); + expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/teamai'))).toBe(false); }); it('does not delete leftover copies on out-of-whitelist tools', async () => { diff --git a/src/__tests__/push-namespace-e2e.test.ts b/src/__tests__/push-namespace-e2e.test.ts index 787608b8..0811b50d 100644 --- a/src/__tests__/push-namespace-e2e.test.ts +++ b/src/__tests__/push-namespace-e2e.test.ts @@ -365,14 +365,13 @@ describe('push places new rules and agents in a namespace (issue #649)', () => { // The root copy is the author's own, placed under rules/fe-know/. Without // the placedRules redirect in the pre-push sync it stayed at v1, read as a - // local modification, and reverted the teammate's update. (The pull above - // also installs the CLI's built-in `teamai` skill, which this run does - // push — the assertion is about the rule.) + // local modification, and reverted the teammate's update. The built-in + // `teamai` skill the pull installed is CLI-owned and not scanned (#730), + // so once the rule is synced there is nothing left to push. expect(result.output).not.toContain('[rules] my-rule'); + expect(result.output).toContain('No new or modified resources to push'); expect(fs.readFileSync(path.join(fixture.projectRoot, '.claude/rules', 'my-rule.md'), 'utf8')) .toContain('Teammate v2'); - const { branch } = branchFiles(fixture); - expect(git(['show', `${branch}:rules/fe-know/my-rule.md`], fixture.remote)).toContain('Teammate v2'); expect(git(['show', 'main:rules/fe-know/my-rule.md'], fixture.remote)).toContain('Teammate v2'); }, 60_000); diff --git a/src/__tests__/recall-toggle.test.ts b/src/__tests__/recall-toggle.test.ts index 8b18ccda..93f2c502 100644 --- a/src/__tests__/recall-toggle.test.ts +++ b/src/__tests__/recall-toggle.test.ts @@ -2,6 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import path from 'node:path'; import os from 'node:os'; import fse from 'fs-extra'; +import { shipped, shippedSkillDigestsMock } from './helpers/shipped-skills.js'; + +// A CLI-owned file is one whose content a release shipped; `shipped()` is it here. +vi.mock('../packaged-skill-digests.js', () => shippedSkillDigestsMock()); const mockAutoDetectInit = vi.fn(); const mockSaveLocalConfigForScope = vi.fn(); @@ -85,6 +89,29 @@ describe('recall toggle native agent cleanup', () => { expect(await fse.pathExists(legacyMarkdownAgent)).toBe(false); }); + it('disable removes the legacy share skill an earlier release deployed, and nothing beside it', async () => { + const { localConfig, teamConfig } = await mockAutoDetectInit(); + mockAutoDetectInit.mockResolvedValue({ + localConfig, + teamConfig: { ...teamConfig, toolPaths: { codex: { agents: '.codex/agents', skills: '.codex/skills' } } }, + }); + const skillsDir = path.join(homeDir, '.codex', 'skills'); + for (const name of ['teamai-share-learnings', 'team-wiki-codebase', 'teamai', 'my-own']) { + await fse.ensureDir(path.join(skillsDir, name)); + await fse.writeFile(path.join(skillsDir, name, 'SKILL.md'), name === 'my-own' ? '# mine' : shipped(name, 'SKILL.md')); + } + + await recallDisable({}); + + // Upgrade, then `recall disable` before the first pull: the old share + // workflow must not stay discoverable. The stub and the user's skills are + // not recall artifacts; the wiki tree is pull's to remove. + expect(await fse.pathExists(path.join(skillsDir, 'teamai-share-learnings'))).toBe(false); + for (const kept of ['team-wiki-codebase', 'teamai', 'my-own']) { + expect(await fse.pathExists(path.join(skillsDir, kept, 'SKILL.md')), kept).toBe(true); + } + }); + it('disable preserves non-agent files that only share the recall stem', async () => { const backup = path.join(homeDir, '.codex', 'agents', 'teamai-recall.backup'); await fse.writeFile(backup, 'user backup'); @@ -139,7 +166,7 @@ describe('recall toggle native agent cleanup', () => { await expect(fse.pathExists(path.join( copilotHome, 'skills', - 'teamai-share-learnings', + 'teamai', 'SKILL.md', ))).resolves.toBe(true); @@ -156,11 +183,14 @@ describe('recall toggle native agent cleanup', () => { 'agents', 'teamai-recall.agent.md', ))).resolves.toBe(false); + // The deployed stub routes to every workflow, recall-dependent or not, so + // disabling recall no longer removes a skill directory. await expect(fse.pathExists(path.join( copilotHome, 'skills', - 'teamai-share-learnings', - ))).resolves.toBe(false); + 'teamai', + 'SKILL.md', + ))).resolves.toBe(true); }); }); diff --git a/src/__tests__/skill-commands-exist.test.ts b/src/__tests__/skill-commands-exist.test.ts new file mode 100644 index 00000000..f4180f57 --- /dev/null +++ b/src/__tests__/skill-commands-exist.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { Command } from 'commander'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const SKILL_DATA = path.join(ROOT, 'skill-data'); +/** The deployed stub is the one file agents always hold, so its commands are checked too. */ +const DEPLOYED_STUB = path.join(ROOT, 'skills', 'teamai', 'SKILL.md'); + +/** Every `teamai …` invocation written in the served skill content. */ +interface Invocation { + file: string; + line: number; + text: string; +} + +function markdownFiles(dir: string): string[] { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return markdownFiles(full); + return entry.isFile() && entry.name.endsWith('.md') ? [full] : []; + }); +} + +function collectInvocations(): Invocation[] { + const found: Invocation[] = []; + for (const file of [...markdownFiles(SKILL_DATA), DEPLOYED_STUB]) { + const relative = path.relative(ROOT, file); + const lines = fs.readFileSync(file, 'utf8').split('\n'); + + // A description sentence naming the CLI is prose, not an invocation. + let start = 0; + if (lines[0] === '---') { + const close = lines.indexOf('---', 1); + if (close > 0) start = close + 1; + } + + lines.slice(start).forEach((line, offset) => { + // Inside backticks, or a bare command line inside a fenced block. + const spans = [...line.matchAll(/`([^`]*)`/g)].map((m) => m[1]); + if (/^\s*teamai\s/.test(line)) spans.push(line.trim()); + for (const span of spans) { + const text = span.trim().replace(/\s+#.*$/, ''); + // The token after `teamai` has to look like a command name; prose such + // as "teamai — Team AI …" or "teamai …" is a mention, not a call. + if (!/^teamai\s+(-{1,2}[a-z]|[a-z][a-z-]*(\s|$))/.test(text)) continue; + found.push({ file: relative, line: start + offset + 1, text }); + } + }); + } + return found; +} + +/** Walk the command chain an invocation names, and return the command it lands on. */ +function resolveCommand(program: Command, tokens: string[]): { command: Command; rest: string[] } { + let command = program; + let index = 0; + while (index < tokens.length) { + const next = command.commands.find( + (c) => c.name() === tokens[index] || c.aliases().includes(tokens[index]), + ); + if (!next) break; + command = next; + index += 1; + } + return { command, rest: tokens.slice(index) }; +} + +function knownFlags(command: Command, program: Command): Set<string> { + const flags = new Set<string>(['--help', '-h']); + for (const option of [...command.options, ...program.options]) { + if (option.long) flags.add(option.long); + if (option.short) flags.add(option.short); + } + return flags; +} + +function validate(program: Command, invocations: Invocation[]): string[] { + const problems: string[] = []; + + for (const invocation of invocations) { + const tokens = invocation.text.split(/\s+/).slice(1).filter(Boolean); + if (tokens.length === 0) continue; + + const { command, rest } = resolveCommand(program, tokens); + const where = `${invocation.file}:${invocation.line} ${invocation.text}`; + + if (command === program && !tokens[0].startsWith('-')) { + problems.push(`${where} → unknown command "${tokens[0]}"`); + continue; + } + + const flags = knownFlags(command, program); + for (const token of rest) { + if (!token.startsWith('-') || token === '-') continue; + const flag = token.split('=')[0]; + // Placeholders and prose inside an example are not flags to resolve. + if (/[<>[\]{}"']/.test(flag)) continue; + if (!flags.has(flag)) { + problems.push(`${where} → unknown flag "${flag}" for \`teamai ${command.name()}\``); + } + } + } + + return problems; +} + +describe('commands named by the served skill content', () => { + it('all exist in the CLI command table', async () => { + vi.stubEnv('TEAMAI_COMMAND_TABLE_ONLY', '1'); + const { program } = await import('../index.js'); + + const problems = validate(program, collectInvocations()); + expect(problems, `\n${problems.join('\n')}\n`).toEqual([]); + }); + + it('catches the drift it exists to catch', async () => { + vi.stubEnv('TEAMAI_COMMAND_TABLE_ONLY', '1'); + const { program } = await import('../index.js'); + + // `teamai extract graph` is the command the wiki skill advertised until this + // change (issue #678, defect D1); the flag is invented. + const problems = validate(program, [ + { file: 'synthetic.md', line: 1, text: 'teamai extract graph' }, + { file: 'synthetic.md', line: 2, text: 'teamai codebase --no-such-flag' }, + ]); + + expect(problems).toHaveLength(2); + expect(problems[0]).toContain('unknown command "extract"'); + expect(problems[1]).toContain('unknown flag "--no-such-flag"'); + }); +}); diff --git a/src/__tests__/skill-content.test.ts b/src/__tests__/skill-content.test.ts new file mode 100644 index 00000000..c3dd9596 --- /dev/null +++ b/src/__tests__/skill-content.test.ts @@ -0,0 +1,408 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + SKILL_DIR_PLACEHOLDER, + listServableSkills, + packagedSkillRoots, + renderSkill, + resolveServableSkill, + skillCatalog, + skillGet, + skillPath, + type PackagedSkill, + type PackagedSkillRoots, +} from '../skill-content.js'; +import { readSkillDescription } from '../agent-skills.js'; +import { listFilesRecursive } from '../utils/fs.js'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + +/** Build a throwaway package layout: <tmp>/skills and <tmp>/skill-data. */ +function makeRoots(): { tmp: string; deployRoot: string; dataRoot: string } { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-skill-content-')); + return { + tmp, + deployRoot: path.join(tmp, 'skills'), + dataRoot: path.join(tmp, 'skill-data'), + }; +} + +function writeSkill(root: string, name: string, body: string, files: Record<string, string> = {}): string { + const dir = path.join(root, name); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'SKILL.md'), body); + for (const [relative, content] of Object.entries(files)) { + const target = path.join(dir, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); + } + return dir; +} + +/** Resolve or fail the test, so the assertions below need no non-null operator. */ +async function mustResolve(name: string, roots: PackagedSkillRoots): Promise<PackagedSkill> { + const resolved = await resolveServableSkill(name, roots); + if (resolved.kind !== 'found') throw new Error(`fixture skill not found: ${name} (${resolved.kind})`); + return resolved.skill; +} + +/** The name a resolution lands on, or null: what the alias assertions compare. */ +async function resolvedName(name: string, roots: PackagedSkillRoots): Promise<string | null> { + const resolved = await resolveServableSkill(name, roots); + if (resolved.kind === 'not-found') return null; + return resolved.kind === 'found' ? resolved.skill.name : resolved.name; +} + +describe('packaged skill discovery', () => { + let roots: ReturnType<typeof makeRoots>; + + beforeEach(() => { + roots = makeRoots(); + }); + + afterEach(() => { + fs.rmSync(roots.tmp, { recursive: true, force: true }); + }); + + it('serves skill-data/ when it exists', async () => { + writeSkill(roots.deployRoot, 'teamai', '# stub\n'); + writeSkill(roots.dataRoot, 'core', '# core\n'); + writeSkill(roots.dataRoot, 'wiki', '# wiki\n'); + + const servable = await listServableSkills(roots); + expect(servable.map((s) => s.name)).toEqual(['core', 'wiki']); + expect(servable.every((s) => s.deployed)).toBe(false); + }); + + it('serves nothing when only the deployed stub is packaged', async () => { + writeSkill(roots.deployRoot, 'teamai', '# hub\n'); + + // A package without skill-data is broken, not a fallback to serving stubs: + // `skill get` reports it and says to reinstall. + expect(await listServableSkills(roots)).toEqual([]); + }); + + it('keeps the deployed stub reachable by its exact name', async () => { + writeSkill(roots.deployRoot, 'teamai', '# stub\n'); + writeSkill(roots.dataRoot, 'core', '# core\n'); + + const stub = await mustResolve('teamai', roots); + expect(stub.dir).toBe(path.join(roots.deployRoot, 'teamai')); + expect(stub?.deployed).toBe(true); + }); + + it('resolves legacy directory names as aliases', async () => { + writeSkill(roots.dataRoot, 'core', '# core\n'); + writeSkill(roots.dataRoot, 'wiki', '# wiki\n'); + writeSkill(roots.dataRoot, 'share', '# share\n'); + + for (const alias of ['wiki', 'codebase', 'team-wiki-codebase']) { + expect(await resolvedName(alias, roots), alias).toBe('wiki'); + } + for (const alias of ['share', 'learning', 'learnings', 'teamai-share-learnings']) { + expect(await resolvedName(alias, roots), alias).toBe('share'); + } + expect(await resolvedName('default', roots)).toBe('core'); + expect(await resolvedName('nope', roots)).toBeNull(); + }); + + it('ignores directories without SKILL.md and dotfiles', async () => { + fs.mkdirSync(path.join(roots.dataRoot, 'empty'), { recursive: true }); + fs.mkdirSync(path.join(roots.dataRoot, '.hidden'), { recursive: true }); + fs.writeFileSync(path.join(roots.dataRoot, '.hidden', 'SKILL.md'), '# no\n'); + writeSkill(roots.dataRoot, 'core', '# core\n'); + + expect((await listServableSkills(roots)).map((s) => s.name)).toEqual(['core']); + }); +}); + +describe('renderSkill', () => { + let roots: ReturnType<typeof makeRoots>; + + beforeEach(() => { + roots = makeRoots(); + }); + + afterEach(() => { + fs.rmSync(roots.tmp, { recursive: true, force: true }); + }); + + it('prints SKILL.md unchanged, frontmatter included', async () => { + const body = '---\nname: core\ndescription: d\n---\n\n# core\n\nbody text\n'; + writeSkill(roots.dataRoot, 'core', body); + + const skill = await mustResolve('core', roots); + expect(await renderSkill(skill)).toBe(body); + }); + + it('appends references/ then templates/, recursively, sorted by relative path', async () => { + writeSkill(roots.dataRoot, 'wiki', '# wiki\n', { + 'references/methodology/phase1.md': 'phase one\n', + 'references/methodology/phase0.md': 'phase zero\n', + 'references/agents/kb.md': 'kb agent\n', + 'templates/report.md': 'report\n', + }); + + const skill = await mustResolve('wiki', roots); + const out = await renderSkill(skill, { full: true }); + + expect(out).toBe( + '# wiki\n' + + '\n--- references/agents/kb.md ---\n\nkb agent\n' + + '\n--- references/methodology/phase0.md ---\n\nphase zero\n' + + '\n--- references/methodology/phase1.md ---\n\nphase one\n' + + '\n--- templates/report.md ---\n\nreport\n', + ); + }); + + it('resolves {SKILL_DIR} to the packaged directory, in the body and in references', async () => { + writeSkill(roots.dataRoot, 'wiki', `run python3 ${SKILL_DIR_PLACEHOLDER}/scripts/scan_repo.py\n`, { + 'references/howto.md': `see ${SKILL_DIR_PLACEHOLDER}/scripts/\n`, + }); + + const skill = await mustResolve('wiki', roots); + const out = await renderSkill(skill, { full: true }); + + expect(out).not.toContain(SKILL_DIR_PLACEHOLDER); + expect(out).toContain(`python3 ${skill.dir}/scripts/scan_repo.py`); + expect(out).toContain(`see ${skill.dir}/scripts/`); + }); + + it('adds a trailing newline to files that lack one', async () => { + writeSkill(roots.dataRoot, 'core', '# core'); + const skill = await mustResolve('core', roots); + expect(await renderSkill(skill)).toBe('# core\n'); + }); +}); + +describe('skillCatalog', () => { + it('reports name, description and path for each served skill', async () => { + const roots = makeRoots(); + try { + writeSkill(roots.dataRoot, 'core', '---\nname: core\ndescription: Daily sync\n---\n\n# core\n'); + const catalog = await skillCatalog(roots); + expect(catalog).toEqual([ + { name: 'core', description: 'Daily sync', path: path.join(roots.dataRoot, 'core'), deployed: false, blockedBy: null }, + ]); + } finally { + fs.rmSync(roots.tmp, { recursive: true, force: true }); + } + }); +}); + +describe('teamai skill get / path against the shipped package', () => { + let stdout: string; + let stderr: string; + const restore: Array<() => void> = []; + + beforeEach(() => { + stdout = ''; + stderr = ''; + process.exitCode = undefined; + + const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + stdout += String(chunk); + return true; + }); + const logSpy = vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + stdout += args.join(' ') + '\n'; + }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + stderr += args.join(' ') + '\n'; + }); + restore.push(() => writeSpy.mockRestore(), () => logSpy.mockRestore(), () => errorSpy.mockRestore()); + }); + + afterEach(() => { + while (restore.length > 0) restore.pop()?.(); + process.exitCode = undefined; + }); + + it('resolves its roots inside the package', () => { + const roots = packagedSkillRoots(); + expect(roots.deployRoot).toBe(path.join(ROOT, 'skills')); + expect(roots.dataRoot).toBe(path.join(ROOT, 'skill-data')); + }); + + it('prints a shipped skill byte for byte, bar the resolved {SKILL_DIR}', async () => { + const [first] = await listServableSkills(); + await skillGet([first.name]); + + const raw = fs.readFileSync(path.join(first.dir, 'SKILL.md'), 'utf8'); + expect(process.exitCode).toBeUndefined(); + expect(stderr).toBe(''); + expect(stdout).toBe(raw.split(SKILL_DIR_PLACEHOLDER).join(first.dir)); + }); + + it('fails on an unknown name without writing to stdout', async () => { + await skillGet(['no-such-skill']); + + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('Skill not found: no-such-skill'); + expect(stderr).toContain('Available:'); + }); + + it('warns about an unknown flag and still serves the skill', async () => { + const [first] = await listServableSkills(); + await skillGet(['--bogus', first.name]); + + expect(process.exitCode).toBeUndefined(); + expect(stderr).toContain('Unknown flag ignored: --bogus'); + expect(stdout).toBe(await renderSkill(first)); + }); + + it('fails when no name is left after dropping flags', async () => { + await skillGet(['--full']); + + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('No skill name provided'); + }); + + it('separates multiple skills and serves them all with --all', async () => { + // The recall gate applies to --all too (covered in skill-recall-gate.test); + // this run's team config decides whether share is in the dump. + const servable: PackagedSkill[] = []; + for (const skill of await listServableSkills()) { + const resolved = await resolveServableSkill(skill.name); + if (resolved.kind === 'found') servable.push(resolved.skill); + } + await skillGet([], { all: true }); + + const expected = (await Promise.all(servable.map((skill) => renderSkill(skill)))).join('\n---\n\n'); + expect(process.exitCode).toBeUndefined(); + expect(stdout).toBe(expected); + }); + + it('prints the packaged directory of the named skill', async () => { + const [first] = await listServableSkills(); + await skillPath(first.name); + expect(stdout.trim()).toBe(first.dir); + expect(fs.existsSync(path.join(stdout.trim(), 'SKILL.md'))).toBe(true); + }); + + it('fails on an unknown name for path too', async () => { + await skillPath('no-such-skill'); + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('Skill not found: no-such-skill'); + }); +}); + +describe('the shipped skill-data content', () => { + it('names every skill after its directory, and promises no permissions it cannot grant', async () => { + for (const skill of await listServableSkills()) { + const text = fs.readFileSync(path.join(skill.dir, 'SKILL.md'), 'utf8'); + // A frontmatter name that disagrees with the directory makes the skill + // undiscoverable for the agent and unresolvable for `skill get`. + expect(text, skill.name).toMatch(new RegExp(`^name: ${skill.name}$`, 'm')); + // `skill get` prints this frontmatter as command output; the agent never + // processes it as skill metadata, so an `allowed-tools` line here would + // claim grants that do not happen. Only the deployed stub's counts. + expect(text, skill.name).not.toMatch(/^allowed-tools:/m); + } + }); + + it('keeps the deployed stub declaring its own name and tools', () => { + const stub = fs.readFileSync(path.join(ROOT, 'skills/teamai/SKILL.md'), 'utf8'); + expect(stub).toMatch(/^name: teamai$/m); + // The stub is the always-loaded unit, so it pre-approves only the read-only + // `teamai skill …` commands it asks for. Everything else a served workflow + // runs goes through the agent's own permission prompt. + expect(stub).toMatch(/^allowed-tools: Bash\(teamai skill:\*\), Bash\(npx teamai-cli skill:\*\)$/m); + }); + + it('quotes {SKILL_DIR} and $(teamai skill path …) in every command it tells the agent to run', async () => { + // The placeholder resolves to the install path, which can hold a space + // ("Program Files", "~/Library/Application Support", a user's full name) or + // be a Windows path used through Bash. An unquoted occurrence in a command + // line splits into two arguments there and the documented invocation fails. + const offenders: string[] = []; + for (const skill of await listServableSkills()) { + const files = [ + 'SKILL.md', + ...(await listFilesRecursive(path.join(skill.dir, 'references'))).map((f) => `references/${f}`), + ]; + for (const relative of files) { + if (!relative.endsWith('.md')) continue; + const text = fs.readFileSync(path.join(skill.dir, relative), 'utf8'); + text.split('\n').forEach((line, i) => { + // A command word followed by the bare placeholder: `python3 {SKILL_DIR}/…`. + // Prose and reference tables name the path without running it, and a + // quoted occurrence is already correct. + if (/(?:^|[`\s(])(?:python3?|node|bash|sh|cp|mv|cat|ls|rm)\s+\{SKILL_DIR\}/.test(line)) { + offenders.push(`${skill.name}/${relative}:${i + 1}: ${line.trim()}`); + } + // `$(teamai skill path …)` is word-split in a shell command just the + // same, so it is always written inside double quotes. + if (/(?<!")\$\(teamai skill path /.test(line)) { + offenders.push(`${skill.name}/${relative}:${i + 1}: ${line.trim()}`); + } + }); + } + } + expect(offenders).toEqual([]); + }); + + it('ships no Chinese text in the deployed stub or the served content', async () => { + // Both reach the agent as CLI output (`teamai skill get` prints skill-data/), + // which the repo rule keeps English; the agent translates for the user. + const offenders: string[] = []; + for (const root of ['skills', 'skill-data']) { + for (const relative of await listFilesRecursive(path.join(ROOT, root))) { + const text = fs.readFileSync(path.join(ROOT, root, relative), 'utf8'); + text.split('\n').forEach((line, i) => { + if (/[\u3000-\u303f\u3400-\u9fff\uf900-\ufaff\uff00-\uffef]/.test(line)) offenders.push(`${root}/${relative}:${i + 1}`); + }); + } + } + expect(offenders).toEqual([]); + }); + + it('keeps the tests\' stand-in for shipped content on the paths the real digests record', async () => { + // The prune tests mock the digest table; a path they know and the real + // table does not (or the reverse) would test a prune that never runs. + const { PACKAGED_SKILL_DIGESTS } = await import('../packaged-skill-digests.js'); + const { shippedSkillDigestsMock } = await import('./helpers/shipped-skills.js'); + const paths = (table: ReadonlyMap<string, ReadonlyMap<string, readonly string[]>>) => + Object.fromEntries([...table].map(([skill, files]) => [skill, [...files.keys()].sort()])); + expect(paths(shippedSkillDigestsMock().PACKAGED_SKILL_DIGESTS)).toEqual(paths(PACKAGED_SKILL_DIGESTS)); + }); + + it('keeps the stub description within the 1024-character budget agents load it under', async () => { + // With one deployed skill, this description is the only text an agent sees + // at selection time, and hosts cap it at 1024 characters. + const description = await readSkillDescription(path.join(ROOT, 'skills/teamai/SKILL.md')); + expect(description.length).toBeGreaterThan(0); + expect(description.length).toBeLessThanOrEqual(1024); + }); +}); + +describe('npm package contents', () => { + // The whole design fails silently when skill-data/ is missing from + // package.json "files": every test above still passes against the repo, and + // `skill get` serves nothing at all once installed from the registry. + it('ships both the deployed stub and the served content', () => { + const packed = execFileSync('npm', ['pack', '--dry-run', '--json'], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + const files = (JSON.parse(packed) as Array<{ files: Array<{ path: string }> }>)[0] + .files.map((f) => f.path); + + expect(files).toContain('skills/teamai/SKILL.md'); + for (const skill of ['core', 'share', 'wiki']) { + expect(files.some((f) => f.startsWith(`skill-data/${skill}/`)), skill).toBe(true); + } + expect(files).toContain('skill-data/wiki/scripts/scan_repo.py'); + // Running the wiki scripts (the e2e suite does) leaves __pycache__ beside + // them; "files" must not sweep interpreter bytecode into the package. + expect(files.filter((f) => f.endsWith('.pyc') || f.includes('__pycache__'))).toEqual([]); + }, 60_000); +}); diff --git a/src/__tests__/skill-list-uninitialized.test.ts b/src/__tests__/skill-list-uninitialized.test.ts new file mode 100644 index 00000000..d6b4384f --- /dev/null +++ b/src/__tests__/skill-list-uninitialized.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { autoDetectInit, logDim, NotInitializedError } = vi.hoisted(() => ({ + autoDetectInit: vi.fn(), + logDim: vi.fn(), + NotInitializedError: class NotInitializedError extends Error {}, +})); +vi.mock('../config.js', () => ({ autoDetectInit, NotInitializedError })); +vi.mock('../utils/logger.js', () => ({ + log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), dim: logDim }, + setStderrOnly: vi.fn(() => false), +})); + +import { skillList } from '../skill-cmd.js'; + +/** + * `skill get` serves the packaged content on a machine with no team; the + * human-readable `skill list` must let that machine discover it too, instead of + * failing on the team listing it prints first. + */ +describe('teamai skill list before init', () => { + let stdout: string; + let logSpy: ReturnType<typeof vi.spyOn>; + + beforeEach(() => { + stdout = ''; + autoDetectInit.mockReset(); + logDim.mockReset(); + logSpy = vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + stdout += args.join(' ') + '\n'; + }); + }); + + afterEach(() => { + logSpy.mockRestore(); + process.exitCode = undefined; + }); + + it('prints the packaged catalog and says what to run for the rest', async () => { + autoDetectInit.mockRejectedValue(new NotInitializedError('teamai is not initialized. Run `teamai init` first.')); + + await skillList({}); + + expect(process.exitCode).toBeUndefined(); + expect(stdout).toContain('=== BUILT-IN SKILLS (served by the CLI) ==='); + for (const name of ['core', 'setup', 'share', 'wiki']) { + expect(stdout).toContain(`teamai skill get ${name}`); + } + expect(logDim).toHaveBeenCalledWith(expect.stringContaining('teamai init')); + }); + + it('reports a broken config instead of calling the machine uninitialized', async () => { + // A config that exists but cannot be used is not "no team": telling the + // member to run `teamai init` would send them to re-init over a real setup. + autoDetectInit.mockRejectedValue(new Error('Team config (teamai.yaml) not found. Check your repo path.')); + + await expect(skillList({})).rejects.toThrow('Team config (teamai.yaml) not found'); + expect(logDim).not.toHaveBeenCalledWith(expect.stringContaining('Not initialized')); + }); +}); diff --git a/src/__tests__/skill-recall-gate.test.ts b/src/__tests__/skill-recall-gate.test.ts new file mode 100644 index 00000000..3fa9223c --- /dev/null +++ b/src/__tests__/skill-recall-gate.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +const autoDetectInit = vi.fn(); +const findUnreadableProjectConfig = vi.fn(); +vi.mock('../config.js', async (importOriginal) => ({ + ...(await importOriginal<typeof import('../config.js')>()), + autoDetectInit, + findUnreadableProjectConfig, +})); + +import { resolveServableSkill, skillCatalog, skillGet, skillPath } from '../skill-content.js'; + +/** + * Recall used to be decided when deploying: the share skill simply was not + * copied into the agent. One deployed stub routes to every workflow, so the + * decision moved to the moment the agent asks for the content (#678). + */ +describe('recall gate on served skills', () => { + let stderr: string; + let stdout: string; + const restore: Array<() => void> = []; + + beforeEach(() => { + stderr = ''; + stdout = ''; + process.exitCode = undefined; + autoDetectInit.mockReset(); + findUnreadableProjectConfig.mockReset(); + findUnreadableProjectConfig.mockResolvedValue(null); + + const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + stdout += String(chunk); + return true; + }); + const logSpy = vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => { + stdout += args.join(' ') + '\n'; + }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + stderr += args.join(' ') + '\n'; + }); + restore.push(() => writeSpy.mockRestore(), () => logSpy.mockRestore(), () => errorSpy.mockRestore()); + }); + + afterEach(() => { + while (restore.length > 0) restore.pop()?.(); + process.exitCode = undefined; + }); + + const withRecall = (enabled: boolean): void => { + autoDetectInit.mockResolvedValue({ + localConfig: { recallEnabled: enabled }, + teamConfig: { sharing: { recall: { enabled } } }, + }); + }; + + it('blocks share when recall is disabled, and says what to turn on', async () => { + withRecall(false); + + expect(await resolveServableSkill('share')).toEqual({ kind: 'blocked', name: 'share', reason: 'recall' }); + // Aliases land on the same gate: the name in the refusal is the canonical one. + expect(await resolveServableSkill('teamai-share-learnings')).toMatchObject({ kind: 'blocked', name: 'share' }); + + await skillGet(['share']); + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('share needs recall'); + expect(stderr).toContain('teamai recall enable'); + }); + + it('serves share when recall is enabled', async () => { + withRecall(true); + + expect(await resolveServableSkill('share')).toMatchObject({ kind: 'found', skill: { name: 'share' } }); + + await skillGet(['share']); + expect(process.exitCode).toBeUndefined(); + expect(stdout).toContain('name: share'); + }); + + it('leaves share out of --all when recall is disabled, and says so on stderr', async () => { + withRecall(false); + + await skillGet([], { all: true }); + expect(process.exitCode).toBeUndefined(); + expect(stdout).toContain('name: core'); + expect(stdout).toContain('name: wiki'); + expect(stdout).not.toContain('name: share'); + expect(stderr).toContain('Skipped share'); + expect(stderr).toContain('teamai recall enable'); + }); + + it('withholds the share directory from skill path and the catalog when recall is disabled', async () => { + withRecall(false); + + await skillPath('share'); + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('share needs recall'); + + const share = (await skillCatalog()).find((entry) => entry.name === 'share'); + expect(share).toMatchObject({ blockedBy: 'recall', path: null }); + }); + + it('serves the share directory through skill path and the catalog when recall is enabled', async () => { + withRecall(true); + + await skillPath('share'); + expect(process.exitCode).toBeUndefined(); + expect(stdout.trim()).toMatch(/skill-data[\\/]share$/); + + const share = (await skillCatalog()).find((entry) => entry.name === 'share'); + expect(share).toMatchObject({ blockedBy: null, path: stdout.trim() }); + }); + + it('withholds share from a read-only HTTP team, whose `teamai contribute` always refuses', async () => { + // Recall on, so only the source decides: the workflow's last step would fail + // after the agent had written the whole learning. + autoDetectInit.mockResolvedValue({ + localConfig: { recallEnabled: true, repo: { kind: 'http', localPath: '/tmp', remote: '' } }, + teamConfig: { sharing: { recall: { enabled: true } } }, + }); + + expect(await resolveServableSkill('share')).toEqual({ kind: 'blocked', name: 'share', reason: 'read-only' }); + await skillGet(['share']); + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('read-only HTTP source'); + expect(stderr).not.toContain('teamai recall enable'); + expect((await skillCatalog()).find((entry) => entry.name === 'share')).toMatchObject({ blockedBy: 'read-only', path: null }); + }); + + it('keeps a config-migration line off stdout, so the content and the JSON stay exact', async () => { + // Loading an upgraded config can migrate it and say so with log.info. + autoDetectInit.mockImplementation(async () => { + const { log } = await import('../utils/logger.js'); + log.info('Migrated legacy teamai config to default role profile: hai'); + return { localConfig: { recallEnabled: true }, teamConfig: { sharing: { recall: { enabled: true } } } }; + }); + + await skillGet(['share']); + expect(stdout.startsWith('---\nname: share')).toBe(true); + expect(stdout).not.toContain('Migrated legacy'); + expect(stderr).toContain('Migrated legacy'); + + stdout = ''; + const { skillList } = await import('../skill-cmd.js'); + await skillList({ json: true }); + expect(() => JSON.parse(stdout)).not.toThrow(); + }); + + it('never gates the skills that do not depend on recall', async () => { + withRecall(false); + + for (const name of ['core', 'setup', 'wiki']) { + expect((await resolveServableSkill(name)).kind, name).toBe('found'); + } + }); + + it('fails open when there is no team config to consult', async () => { + const { NotInitializedError } = await import('../config.js'); + autoDetectInit.mockRejectedValue(new NotInitializedError('teamai is not initialized. Run `teamai init` first.')); + + // A fresh machine reading the docs gets the content, not a refusal it + // cannot act on. + expect((await resolveServableSkill('share')).kind).toBe('found'); + }); + + it('blocks share when a config exists but cannot be loaded, since recall and the source are then unknown', async () => { + autoDetectInit.mockRejectedValue(new Error('Team config (teamai.yaml) not found. Check your repo path.')); + + expect(await resolveServableSkill('share')).toEqual({ kind: 'blocked', name: 'share', reason: 'config' }); + await skillGet(['share']); + expect(process.exitCode).toBe(1); + expect(stdout).toBe(''); + expect(stderr).toContain('config on this machine could not be loaded'); + expect((await skillCatalog()).find((entry) => entry.name === 'share')).toMatchObject({ blockedBy: 'config', path: null }); + // Only share depends on the config; the rest is still served. + expect((await resolveServableSkill('core')).kind).toBe('found'); + }); + + it('blocks share when the project config is unreadable, instead of answering with the user config', async () => { + // Detection skips the broken project config; the user config it falls back + // to belongs to another team, with its own recall and source. + findUnreadableProjectConfig.mockResolvedValue('/work/proj/.teamai/config.yaml: bad indentation'); + withRecall(true); + + expect(await resolveServableSkill('share')).toEqual({ kind: 'blocked', name: 'share', reason: 'config' }); + expect(autoDetectInit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/skill-show.test.ts b/src/__tests__/skill-show.test.ts index 706a7c66..616c6cfd 100644 --- a/src/__tests__/skill-show.test.ts +++ b/src/__tests__/skill-show.test.ts @@ -12,6 +12,7 @@ vi.mock('../utils/logger.js', () => ({ debug: vi.fn(), dim: vi.fn(), }, + setStderrOnly: vi.fn(() => false), })); import type { LocalConfig, TeamaiConfig } from '../types.js'; @@ -86,7 +87,8 @@ function captureLogs() { } async function runSkillShow(name: string, fx: Fixture): Promise<string[]> { - vi.doMock('../config.js', () => ({ + vi.doMock('../config.js', async (importOriginal) => ({ + ...(await importOriginal<typeof import('../config.js')>()), autoDetectInit: async () => ({ localConfig: fx.localConfig, teamConfig: fx.teamConfig }), })); const { skillShow } = await import('../skill-cmd.js'); @@ -147,6 +149,64 @@ describe('skillShow locator', () => { expect(text).toContain('claude'); }); + it("prefers a member's own skill over a packaged name or alias", async () => { + // `codebase` aliases the wiki skill and `share` is served by the CLI, but a + // directory a member created under either name is the skill they mean. + const claudeSkillsDir = path.join(fx.homeDir, '.claude', 'skills'); + await fse.ensureDir(claudeSkillsDir); + await makeSkill(claudeSkillsDir, 'codebase', 'my own codebase notes'); + await makeSkill(claudeSkillsDir, 'share', 'my own sharing helper'); + + for (const [name, description] of [['codebase', 'my own codebase notes'], ['share', 'my own sharing helper']]) { + const text = (await runSkillShow(name, fx)).join('\n'); + expect(text, name).toContain(description); + expect(text, name).toContain('[local-only]'); + expect(text, name).not.toContain('skill-data'); + // `share` is recall-gated in the package; a member's own skill is not. + expect(process.exitCode, name).toBe(0); + } + }); + + it('classifies a skill served from the package as builtin', async () => { + // Only the deployed stub is in BUILTIN_SKILL_NAMES; the served workflows + // are built in by where they were found, not by name. + const lines = await runSkillShow('core', fx); + const text = lines.join('\n'); + expect(text).toContain('Source : [builtin]'); + expect(text).toContain('Read it with : teamai skill get core'); + expect(text).not.toContain('[local-only]'); + }); + + it('refuses share while recall is disabled, like skill get and skill path do', async () => { + // The fixture's team has no recall setting, so it is off by default. + const lines = await runSkillShow('share', fx); + expect(process.exitCode).toBe(1); + expect(lines.find((l) => l.includes('skill: share'))).toBeUndefined(); + expect(lines.join('\n')).not.toContain('skill-data'); + process.exitCode = 0; + }); + + it('refuses a legacy share directory a pull has not pruned yet, instead of showing its path', async () => { + // A pre-stub release wrote this; it is the CLI's stale copy, not the + // member's skill, so the name must go through the packaged gate. + const claudeSkillsDir = path.join(fx.homeDir, '.claude', 'skills'); + await makeSkill(claudeSkillsDir, 'teamai-share-learnings', 'old share workflow'); + + const lines = await runSkillShow('teamai-share-learnings', fx); + expect(process.exitCode).toBe(1); + expect(lines.join('\n')).not.toContain(path.join(claudeSkillsDir, 'teamai-share-learnings')); + process.exitCode = 0; + }); + + it('shows share once recall is enabled', async () => { + fx.localConfig.recallEnabled = true; + const lines = await runSkillShow('share', fx); + const text = lines.join('\n'); + expect(process.exitCode).toBe(0); + expect(text).toContain('skill: share'); + expect(text).toContain('Source : [builtin]'); + }); + it('exits with non-zero code when skill not found', async () => { process.exitCode = 0; const lines = await runSkillShow('does-not-exist', fx); diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts index b46fe087..75b5ec9a 100644 --- a/src/__tests__/skills.test.ts +++ b/src/__tests__/skills.test.ts @@ -217,6 +217,25 @@ scope: 'user', expect(names).not.toContain('ignored-skill'); }); + it('never offers the directories earlier releases deployed as new skills to push', async () => { + // A member who runs `teamai push --all` after upgrading but before their + // next pull still has the legacy trees on disk; they are the CLI's, not theirs. + for (const legacy of ['team-wiki-codebase', 'teamai-share-learnings', 'teamai']) { + const dir = path.join(homeDir, '.claude/skills', legacy); + await fse.ensureDir(dir); + await fse.writeFile(path.join(dir, 'SKILL.md'), '# packaged by an earlier release'); + } + const mine = path.join(homeDir, '.claude/skills', 'teamai-workflow'); + await fse.ensureDir(mine); + await fse.writeFile(path.join(mine, 'SKILL.md'), '# mine'); + + const names = (await handler.scanLocalForPush(teamConfig, localConfig)).map((i) => i.name); + expect(names).not.toContain('team-wiki-codebase'); + expect(names).not.toContain('teamai-share-learnings'); + expect(names).not.toContain('teamai'); + expect(names).toContain('teamai-workflow'); + }); + it('should detect both new and modified skills together', async () => { // Modified const teamSkillDir = path.join(localConfig.repo.localPath, 'skills', 'existing'); diff --git a/src/__tests__/skip-uninstalled-tools.test.ts b/src/__tests__/skip-uninstalled-tools.test.ts index f4ac0fc8..4cf5a3b6 100644 --- a/src/__tests__/skip-uninstalled-tools.test.ts +++ b/src/__tests__/skip-uninstalled-tools.test.ts @@ -1,7 +1,65 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import path from 'node:path'; import os from 'node:os'; +import { fileURLToPath } from 'node:url'; import fse from 'fs-extra'; +import { listFilesRecursive } from '../utils/fs.js'; +import { shipped, shippedSkillDigestsMock } from './helpers/shipped-skills.js'; + +const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + +/** The team config the prune tests share; pass toolPaths to change which tool runs. */ +function legacyPruneTeamConfig( + toolPaths: Record<string, { skills: string; userScope?: { skills: string } }> = { claude: { skills: '.claude/skills' } }, +) { + return { + team: 'test', + description: '', + repo: 'https://git.woa.com/test/repo.git', + provider: 'tgit' as const, + reviewers: [], + sharing: { + skills: {}, + rules: { enforced: [] }, + docs: { localDir: '' }, + env: { injectShellProfile: true }, + }, + toolPaths, + }; +} + +function legacyPruneLocalConfig(tmpDir: string) { + return { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://git.woa.com/test/repo.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; +} + +/** + * The one backup root this run created. Its name carries a timestamp, so the + * test reads it back instead of reconstructing it and racing the clock. + */ +async function onlyRunDir(homeDir: string): Promise<string> { + const root = path.join(homeDir, '.teamai/removed-skills'); + const runs = await fse.readdir(root); + expect(runs).toHaveLength(1); + // Below the run comes the base directory the deploy targeted, keyed by a + // digest so two scopes in one process cannot land on the same path. + const bases = await fse.readdir(path.join(root, runs[0])); + expect(bases).toHaveLength(1); + return path.join(root, runs[0], bases[0]); +} + +// A file is the CLI's only at content a release shipped; `shipped()` stands in +// for that content, anything else at the same path is the member's. +vi.mock('../packaged-skill-digests.js', () => shippedSkillDigestsMock()); + +const WIKI_SKILL = shipped('team-wiki-codebase', 'SKILL.md'); +/** Two releases' SKILL.md: both ours, and told apart. */ +const WIKI_SKILL_OTHER_RELEASE = shipped('team-wiki-codebase', 'SKILL.md', 2); vi.mock('../config.js', async (importOriginal) => ({ ...(await importOriginal<typeof import('../config.js')>()), @@ -480,11 +538,11 @@ describe('deployBuiltinSkills — skip uninstalled tools', () => { expect(deployed).toBeGreaterThan(0); expect(await fse.pathExists(path.join( homeDir, - '.claude/skills/team-wiki-codebase/SKILL.md', + '.claude/skills/teamai/SKILL.md', ))).toBe(true); }); - it('should recursively deploy nested built-in skill files', async () => { + it('deploys the discovery stub only, never the packaged content', async () => { const { deployBuiltinSkills } = await import('../builtin-skills.js'); const teamConfig = { @@ -513,12 +571,18 @@ describe('deployBuiltinSkills — skip uninstalled tools', () => { }; const deployed = await deployBuiltinSkills(teamConfig, localConfig); - const skillDir = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + const skillsDir = path.join(homeDir, '.claude/skills'); + const stubDir = path.join(skillsDir, 'teamai'); expect(deployed).toBeGreaterThan(0); - expect(await fse.pathExists(path.join(skillDir, 'SKILL.md'))).toBe(true); - expect(await fse.pathExists(path.join(skillDir, 'references/methodology/phase0-collection.md'))).toBe(true); - expect(await fse.pathExists(path.join(skillDir, 'scripts/scan_repo.py'))).toBe(true); + expect(await fse.pathExists(path.join(stubDir, 'SKILL.md'))).toBe(true); + // The stub is the whole deployed unit: one file, no references, no scripts. + expect(await fse.readdir(stubDir)).toEqual(['SKILL.md']); + expect(await fse.readdir(skillsDir)).toEqual(['teamai']); + // ...and it is the packaged file verbatim, so a diff means a bug. + expect(await fse.readFile(path.join(stubDir, 'SKILL.md'), 'utf8')).toBe( + await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'), + ); }); it('deploys built-in skills to OpenCode user scope under .config/opencode/skills', async () => { @@ -559,11 +623,11 @@ describe('deployBuiltinSkills — skip uninstalled tools', () => { expect(deployed).toBeGreaterThan(0); // Written to the user-scope path, NOT the project-scope .opencode/skills. - expect(await fse.pathExists(path.join(homeDir, '.config/opencode/skills/team-wiki-codebase/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.config/opencode/skills/teamai/SKILL.md'))).toBe(true); expect(await fse.pathExists(path.join(homeDir, '.opencode'))).toBe(false); }); - it('should still deploy team-wiki-codebase when recall is disabled (skipRecall)', async () => { + it('deploys the stub regardless of recall, and prunes the legacy directories', async () => { const { deployBuiltinSkills } = await import('../builtin-skills.js'); const teamConfig = { @@ -591,21 +655,679 @@ describe('deployBuiltinSkills — skip uninstalled tools', () => { scope: 'user' as const, }; - const deployed = await deployBuiltinSkills(teamConfig, localConfig, { skipRecall: true }); + // Pre-stub releases left these behind in every agent directory. + await fse.ensureDir(path.join(homeDir, '.claude/skills/team-wiki-codebase/references')); + await fse.writeFile(path.join(homeDir, '.claude/skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + await fse.ensureDir(path.join(homeDir, '.claude/skills/teamai-share-learnings')); + await fse.writeFile(path.join(homeDir, '.claude/skills/teamai-share-learnings/SKILL.md'), shipped('teamai-share-learnings', 'SKILL.md')); + // These two names were reserved in the old guard set but never packaged, so + // a directory by either name is the user's own skill. + for (const userSkill of ['teamai-workflow', 'teamai-import']) { + await fse.ensureDir(path.join(homeDir, `.claude/skills/${userSkill}`)); + await fse.writeFile(path.join(homeDir, `.claude/skills/${userSkill}/SKILL.md`), '# mine'); + } + + const deployed = await deployBuiltinSkills(teamConfig, localConfig); expect(deployed).toBeGreaterThan(0); - expect(await fse.pathExists(path.join(homeDir, '.claude/skills/team-wiki-codebase/SKILL.md'))).toBe(true); - const wikiEnrichFile = path.join( - homeDir, - '.claude/skills/team-wiki-codebase/references/methodology/phase3-ai-enhancement.md', + // The stub routes to every workflow, so recall no longer gates deployment: + // `teamai skill get share` decides at run time whether recall is on, and the + // directories earlier releases deployed are removed on the way. + expect(await fse.pathExists(path.join(homeDir, '.claude/skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.claude/skills/team-wiki-codebase'))).toBe(false); + expect(await fse.pathExists(path.join(homeDir, '.claude/skills/teamai-share-learnings'))).toBe(false); + for (const userSkill of ['teamai-workflow', 'teamai-import']) { + expect(await fse.readFile(path.join(homeDir, `.claude/skills/${userSkill}/SKILL.md`), 'utf8'), userSkill).toBe('# mine'); + } + }); + + it('archives what it prunes, and keeps a packaged path whose content the member changed', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://git.woa.com/test/repo.git', + provider: 'tgit' as const, + reviewers: [], + sharing: { + skills: {}, + rules: { enforced: [] }, + docs: { localDir: '' }, + env: { injectShellProfile: true }, + }, + toolPaths: { + claude: { skills: '.claude/skills' }, + }, + }; + + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://git.woa.com/test/repo.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; + + // A path a release shipped is ours only at content a release shipped there. + // The unedited SKILL.md goes, a copy archived first; the reference the + // member rewrote is theirs now and stays, and so does its directory. + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + await fse.ensureDir(path.join(wiki, 'references/methodology')); + await fse.writeFile(path.join(wiki, 'SKILL.md'), WIKI_SKILL); + await fse.writeFile(path.join(wiki, 'references/methodology/phase0-collection.md'), '# my notes'); + + await deployBuiltinSkills(teamConfig, localConfig); + + expect(await fse.pathExists(path.join(wiki, 'SKILL.md'))).toBe(false); + expect(await fse.readFile(path.join(wiki, 'references/methodology/phase0-collection.md'), 'utf8')).toBe('# my notes'); + + const backup = path.join(await onlyRunDir(homeDir), 'claude/.claude-skills/team-wiki-codebase'); + expect(await fse.readFile(path.join(backup, 'SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(backup, 'references/methodology/phase0-collection.md'))).toBe(false); + }); + + it('keeps a file it could not back up, instead of deleting it anyway', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = legacyPruneTeamConfig(); + const localConfig = legacyPruneLocalConfig(tmpDir); + + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + await fse.ensureDir(wiki); + await fse.writeFile(path.join(wiki, 'SKILL.md'), WIKI_SKILL); + + // A file where the backup tree has to start: every copy under it fails, the + // way a full disk or a read-only home would. + await fse.ensureDir(path.join(homeDir, '.teamai')); + await fse.writeFile(path.join(homeDir, '.teamai/removed-skills'), 'not a directory'); + + await deployBuiltinSkills(teamConfig, localConfig); + + expect(await fse.readFile(path.join(wiki, 'SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + }); + + it('never walks through a symlinked skill root, so it cannot delete the link target', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // A shared checkout the member linked in. Every path under it matches ours + // by name, so following the link would delete files we never wrote. + const shared = path.join(tmpDir, 'shared-skills/team-wiki-codebase'); + await fse.ensureDir(shared); + await fse.writeFile(path.join(shared, 'SKILL.md'), WIKI_SKILL); + + await fse.ensureDir(path.join(homeDir, '.claude/skills')); + await fse.symlink(shared, path.join(homeDir, '.claude/skills/team-wiki-codebase'), 'dir'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(shared, 'SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(homeDir, '.claude/skills/team-wiki-codebase'))).toBe(true); + }); + + it('stops at a link above the skill directory, not just at the skill directory', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // The common shape: the member links their whole skills root at a dotfiles + // checkout. Every directory under it is real, so checking the leaf alone + // sees nothing and the walk deletes files in the checkout. + const dotfiles = path.join(tmpDir, 'dotfiles/skills'); + await fse.ensureDir(path.join(dotfiles, 'team-wiki-codebase')); + await fse.writeFile(path.join(dotfiles, 'team-wiki-codebase/SKILL.md'), WIKI_SKILL); + + await fse.ensureDir(path.join(homeDir, '.claude')); + await fse.symlink(dotfiles, path.join(homeDir, '.claude/skills'), 'dir'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(dotfiles, 'team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(dotfiles, 'teamai/SKILL.md'))).toBe(false); + }); + + it('does not write the stub through a symlinked destination', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const outside = path.join(tmpDir, 'outside/teamai'); + await fse.ensureDir(outside); + await fse.writeFile(path.join(outside, 'SKILL.md'), '# not ours'); + + await fse.ensureDir(path.join(homeDir, '.claude/skills')); + await fse.symlink(outside, path.join(homeDir, '.claude/skills/teamai'), 'dir'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + // The prune refuses to walk the link; the copy must refuse to write through + // it too, or the guarantee stops one line short of where it is claimed. + expect(await fse.readFile(path.join(outside, 'SKILL.md'), 'utf8')).toBe('# not ours'); + }); + + it('stops at a link on any component below the base, not only the last two', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // `~/.config/opencode` linked at a dotfiles checkout: the skills root and + // the skill directory under it are real directories, the link is higher up. + const dotfiles = path.join(tmpDir, 'dotfiles/opencode'); + await fse.ensureDir(path.join(dotfiles, 'skills/team-wiki-codebase')); + await fse.writeFile(path.join(dotfiles, 'skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + await fse.ensureDir(path.join(homeDir, '.config')); + await fse.symlink(dotfiles, path.join(homeDir, '.config/opencode'), 'dir'); + + const deployed = await deployBuiltinSkills( + legacyPruneTeamConfig({ opencode: { skills: '.config/opencode/skills' } }), + legacyPruneLocalConfig(tmpDir), + ); + + expect(deployed).toBe(0); + expect(await fse.readFile(path.join(dotfiles, 'skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(dotfiles, 'skills/teamai'))).toBe(false); + }); + + it('deploys the stub and prunes where team skills land for Hermes and OpenClaw, not under the tool root', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // Hermes honours HERMES_HOME, which can live outside HOME; OpenClaw reads + // skills from its workspace. Team-skill sync already resolves both. + const hermesHome = path.join(tmpDir, 'elsewhere/hermes'); + vi.stubEnv('HERMES_HOME', hermesHome); + await fse.ensureDir(path.join(hermesHome, 'skills/team-wiki-codebase')); + await fse.writeFile(path.join(hermesHome, 'skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + const workspace = path.join(homeDir, '.openclaw/workspace'); + await fse.ensureDir(workspace); + + const deployed = await deployBuiltinSkills( + legacyPruneTeamConfig({ hermes: { skills: '.hermes/skills' }, openclaw: { skills: '.openclaw/skills' } }), + legacyPruneLocalConfig(tmpDir), + ); + + expect(deployed).toBe(2); + expect(await fse.pathExists(path.join(hermesHome, 'skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(hermesHome, 'skills/team-wiki-codebase'))).toBe(false); + expect(await fse.pathExists(path.join(workspace, 'skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.hermes'))).toBe(false); + expect(await fse.pathExists(path.join(homeDir, '.openclaw/skills'))).toBe(false); + }); + + it('refuses a linked HERMES_HOME outside the home directory, the root itself not only what is under it', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const target = path.join(tmpDir, 'dotfiles/hermes'); + await fse.ensureDir(path.join(target, 'skills/team-wiki-codebase')); + await fse.writeFile(path.join(target, 'skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + const hermesHome = path.join(tmpDir, 'elsewhere/hermes'); + await fse.ensureDir(path.dirname(hermesHome)); + await fse.symlink(target, hermesHome, 'dir'); + vi.stubEnv('HERMES_HOME', hermesHome); + + const deployed = await deployBuiltinSkills( + legacyPruneTeamConfig({ hermes: { skills: '.hermes/skills' } }), + legacyPruneLocalConfig(tmpDir), + ); + + expect(deployed).toBe(0); + expect(await fse.readFile(path.join(target, 'skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(target, 'skills/teamai'))).toBe(false); + }); + + it('refuses a linked COPILOT_HOME, which is Copilot\'s own base directory in user scope', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const target = path.join(tmpDir, 'dotfiles/copilot'); + await fse.ensureDir(path.join(target, 'skills/team-wiki-codebase')); + await fse.writeFile(path.join(target, 'skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + await fse.symlink(target, path.join(homeDir, '.copilot'), 'dir'); + + const deployed = await deployBuiltinSkills( + legacyPruneTeamConfig({ copilot: { skills: '.github/skills', userScope: { skills: 'skills' } } }), + legacyPruneLocalConfig(tmpDir), + ); + + expect(deployed).toBe(0); + expect(await fse.readFile(path.join(target, 'skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.pathExists(path.join(target, 'skills/teamai'))).toBe(false); + }); + + it('keeps a skill of the member\'s that only shares a packaged name, whatever its paths', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // A root TeamAI never managed, or a skill the member wrote under the old + // name: every path matches a packaged one, no content matches a release. + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + await fse.ensureDir(path.join(wiki, 'scripts')); + await fse.writeFile(path.join(wiki, 'SKILL.md'), '---\nname: team-wiki-codebase\n---\n# my own wiki skill\n'); + await fse.writeFile(path.join(wiki, 'scripts/scan_repo.py'), 'print("mine")\n'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(wiki, 'SKILL.md'), 'utf8')).toContain('# my own wiki skill'); + expect(await fse.readFile(path.join(wiki, 'scripts/scan_repo.py'), 'utf8')).toBe('print("mine")\n'); + expect(await fse.pathExists(path.join(homeDir, '.teamai/removed-skills'))).toBe(false); + }); + + it('retires the second Codex copy of the stub, so Codex does not read a stale one beside it', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // The resolver picks .agents/skills/teamai because it exists; the copy an + // earlier release left in .codex/skills would otherwise keep its old body. + await fse.ensureDir(path.join(homeDir, '.codex')); + const shared = path.join(homeDir, '.agents/skills/teamai'); + const configured = path.join(homeDir, '.codex/skills/teamai'); + await fse.ensureDir(shared); + await fse.writeFile(path.join(shared, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.ensureDir(path.join(configured, 'references')); + await fse.writeFile(path.join(configured, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.writeFile(path.join(configured, 'references/setup-admin.md'), shipped('teamai', 'references/setup-admin.md')); + + await deployBuiltinSkills(legacyPruneTeamConfig({ codex: { skills: '.codex/skills' } }), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(shared, 'SKILL.md'), 'utf8')).toBe( + await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'), ); - expect(await fse.pathExists(wikiEnrichFile)).toBe(true); - expect(await fse.pathExists(path.join(homeDir, '.claude/skills/teamai-share-learnings/SKILL.md'))).toBe(false); + expect(await fse.pathExists(configured)).toBe(false); + }); + + it('keeps the second Codex copy when it holds a file TeamAI did not write', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + await fse.ensureDir(path.join(homeDir, '.codex')); + const shared = path.join(homeDir, '.agents/skills/teamai'); + const configured = path.join(homeDir, '.codex/skills/teamai'); + await fse.ensureDir(shared); + await fse.ensureDir(path.join(configured, 'references')); + await fse.writeFile(path.join(configured, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.writeFile(path.join(configured, 'references/team-playbook.md'), '# mine'); + + await deployBuiltinSkills(legacyPruneTeamConfig({ codex: { skills: '.codex/skills' } }), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.pathExists(path.join(configured, 'SKILL.md'))).toBe(false); + expect(await fse.readFile(path.join(configured, 'references/team-playbook.md'), 'utf8')).toBe('# mine'); + }); + + it('keeps a member\'s file under a __pycache__ that is not bytecode of a shipped script', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + await fse.ensureDir(path.join(wiki, 'scripts/__pycache__')); + await fse.ensureDir(path.join(wiki, 'notes/__pycache__')); + await fse.writeFile(path.join(wiki, 'SKILL.md'), WIKI_SKILL); + await fse.writeFile(path.join(wiki, 'scripts/scan_repo.py'), shipped('team-wiki-codebase', 'scripts/scan_repo.py')); + await fse.writeFile(path.join(wiki, 'scripts/__pycache__/scan_repo.cpython-311.pyc'), 'bytecode'); + await fse.writeFile(path.join(wiki, 'notes/__pycache__/keep.txt'), '# mine'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.pathExists(path.join(wiki, 'scripts/__pycache__/scan_repo.cpython-311.pyc'))).toBe(false); + expect(await fse.readFile(path.join(wiki, 'notes/__pycache__/keep.txt'), 'utf8')).toBe('# mine'); + }); + + it('keeps both copies when the user and the project scope prune the same skill in one run', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // `inheritUserScope`: user base, then project base, same tool, root and name. + const projectRoot = path.join(tmpDir, 'work/proj'); + const projectConfig = { ...legacyPruneLocalConfig(tmpDir), scope: 'project' as const, projectRoot }; + for (const [base, body] of [[homeDir, WIKI_SKILL], [projectRoot, WIKI_SKILL_OTHER_RELEASE]]) { + await fse.ensureDir(path.join(base, '.claude/skills/team-wiki-codebase')); + await fse.writeFile(path.join(base, '.claude/skills/team-wiki-codebase/SKILL.md'), body); + } + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + await deployBuiltinSkills(legacyPruneTeamConfig(), projectConfig); + + expect(await fse.pathExists(path.join(projectRoot, '.claude/skills/team-wiki-codebase'))).toBe(false); + const archived = (await listFilesRecursive(path.join(homeDir, '.teamai/removed-skills'))) + .filter((f) => f.endsWith('team-wiki-codebase/SKILL.md')); + const bodies = await Promise.all(archived.map((f) => fse.readFile(path.join(homeDir, '.teamai/removed-skills', f), 'utf8'))); + expect(bodies.sort()).toEqual([WIKI_SKILL, WIKI_SKILL_OTHER_RELEASE].sort()); + }); + + it('keeps the legacy skills when the stub could not be deployed, so the agent keeps one to discover', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // The stub destination is a link, so the stub is refused; pruning first + // would leave the agent with neither the old skills nor the new one. + const outside = path.join(tmpDir, 'outside/teamai'); + await fse.ensureDir(outside); + await fse.ensureDir(path.join(homeDir, '.claude/skills/team-wiki-codebase')); + await fse.writeFile(path.join(homeDir, '.claude/skills/team-wiki-codebase/SKILL.md'), WIKI_SKILL); + await fse.symlink(outside, path.join(homeDir, '.claude/skills/teamai'), 'dir'); + + const deployed = await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(deployed).toBe(0); + expect(await fse.readFile(path.join(homeDir, '.claude/skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + }); + + it('reports a legacy directory it emptied but could not remove, instead of calling it removed', async () => { + if (process.getuid?.() === 0) return; // root ignores directory permissions + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + const { log } = await import('../utils/logger.js'); + + // A locked skills root: the files inside the legacy directory can go, the + // directory itself cannot. The stub directory already exists, so the stub + // still deploys and the prune runs. + const skills = path.join(homeDir, '.claude/skills'); + await fse.ensureDir(path.join(skills, 'team-wiki-codebase')); + await fse.writeFile(path.join(skills, 'team-wiki-codebase/SKILL.md'), WIKI_SKILL); + await fse.ensureDir(path.join(skills, 'teamai')); + await fse.writeFile(path.join(skills, 'teamai/SKILL.md'), shipped('teamai', 'SKILL.md')); + (log.warn as ReturnType<typeof vi.fn>).mockClear(); + await fse.chmod(skills, 0o555); + try { + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + } finally { + await fse.chmod(skills, 0o755); + } + + const warnings = (log.warn as ReturnType<typeof vi.fn>).mock.calls.map((c) => String(c[0])); + expect(warnings.filter((w) => w.includes('team-wiki-codebase') && w.includes('could not be deleted'))).toHaveLength(1); + // The stub's own directory is not empty, so it is not reported. + expect(warnings.filter((w) => w.includes(path.join(skills, 'teamai')))).toEqual([]); + }); + + it('keeps a SKILL.md whose member changed only the frontmatter', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // Same body a release shipped, a description of the member's: the skill is + // theirs now, so the whole file is what ownership is proven on. + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + const edited = `---\nname: team-wiki-codebase\ndescription: my wording\n---\n${WIKI_SKILL}`; + await fse.ensureDir(wiki); + await fse.writeFile(path.join(wiki, 'SKILL.md'), edited); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(wiki, 'SKILL.md'), 'utf8')).toBe(edited); + }); + + it('keeps bytecode beside a script the member edited', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + await fse.ensureDir(path.join(wiki, 'scripts/__pycache__')); + await fse.writeFile(path.join(wiki, 'scripts/scan_repo.py'), 'print("my version")\n'); + await fse.writeFile(path.join(wiki, 'scripts/__pycache__/scan_repo.cpython-311.pyc'), 'bytecode of my version'); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.pathExists(path.join(wiki, 'scripts/__pycache__/scan_repo.cpython-311.pyc'))).toBe(true); + }); + + it('keeps the old references when the stub cannot be written over the old SKILL.md', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // Something the copy cannot replace sits where SKILL.md goes, so the stub + // is not written. Pruning the references first would leave the old skill + // pointing at files that are gone. + const stubDir = path.join(homeDir, '.claude/skills/teamai'); + await fse.ensureDir(path.join(stubDir, 'references')); + await fse.ensureDir(path.join(stubDir, 'SKILL.md')); + await fse.writeFile(path.join(stubDir, 'SKILL.md', 'keep'), '# blocks the copy'); + await fse.writeFile(path.join(stubDir, 'references/setup-admin.md'), shipped('teamai', 'references/setup-admin.md')); + + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(stubDir, 'references/setup-admin.md'), 'utf8')).toBe(shipped('teamai', 'references/setup-admin.md')); + }); + + it('deletes nothing through a linked Codex skills root while resolving where the stub goes', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // `.codex/skills` linked at a dotfiles checkout, holding a copy identical to + // the shared one and to the package: the resolver's reconciliation would + // delete it through the link before the guard ever ran. + const stub = await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'); + const dotfiles = path.join(tmpDir, 'dotfiles/codex-skills'); + await fse.ensureDir(path.join(dotfiles, 'teamai')); + await fse.writeFile(path.join(dotfiles, 'teamai/SKILL.md'), stub); + await fse.ensureDir(path.join(homeDir, '.codex')); + await fse.symlink(dotfiles, path.join(homeDir, '.codex/skills'), 'dir'); + await fse.ensureDir(path.join(homeDir, '.agents/skills/teamai')); + await fse.writeFile(path.join(homeDir, '.agents/skills/teamai/SKILL.md'), stub); + + await deployBuiltinSkills(legacyPruneTeamConfig({ codex: { skills: '.codex/skills' } }), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.readFile(path.join(dotfiles, 'teamai/SKILL.md'), 'utf8')).toBe(stub); + }); + + it('archives nothing when there is nothing retired to archive', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + // Deployment runs on every session start, unchanged revision included. The + // stub it rewrites is shipped now, not retired, so it must not be archived + // once per session for the life of the install. + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + await deployBuiltinSkills(legacyPruneTeamConfig(), legacyPruneLocalConfig(tmpDir)); + + expect(await fse.pathExists(path.join(homeDir, '.teamai/removed-skills'))).toBe(false); + }); + + it('gives each skill root its own backup, so the two Codex copies do not overwrite each other', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = legacyPruneTeamConfig({ codex: { skills: '.codex/skills' } }); + const localConfig = legacyPruneLocalConfig(tmpDir); + + // Codex prunes its own root and the shared one; same skill name, different files. + for (const [root, body] of [['.codex/skills', WIKI_SKILL], ['.agents/skills', WIKI_SKILL_OTHER_RELEASE]]) { + await fse.ensureDir(path.join(homeDir, root, 'team-wiki-codebase')); + await fse.writeFile(path.join(homeDir, root, 'team-wiki-codebase/SKILL.md'), body); + } + + await deployBuiltinSkills(teamConfig, localConfig); + + const run = await onlyRunDir(homeDir); + expect(await fse.readFile(path.join(run, 'codex/.codex-skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL); + expect(await fse.readFile(path.join(run, 'codex/.agents-skills/team-wiki-codebase/SKILL.md'), 'utf8')).toBe(WIKI_SKILL_OTHER_RELEASE); + }); + + it('removes the references an earlier release deployed beside the stub', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://git.woa.com/test/repo.git', + provider: 'tgit' as const, + reviewers: [], + sharing: { skills: {}, rules: { enforced: [] }, docs: { localDir: '' }, env: { injectShellProfile: true } }, + toolPaths: { claude: { skills: '.claude/skills' } }, + }; + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://git.woa.com/test/repo.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; + + // What `teamai pull` wrote before the stub: the same directory name, with a + // references tree the new deployment does not ship. + const stubDir = path.join(homeDir, '.claude/skills/teamai'); + await fse.ensureDir(path.join(stubDir, 'references')); + await fse.writeFile(path.join(stubDir, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.writeFile(path.join(stubDir, 'references/setup-admin.md'), shipped('teamai', 'references/setup-admin.md')); + + await deployBuiltinSkills(teamConfig, localConfig); + + expect(await fse.readdir(stubDir)).toEqual(['SKILL.md']); + expect(await fse.readFile(path.join(stubDir, 'SKILL.md'), 'utf8')).toBe( + await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'), + ); + }); + + it('prunes legacy skills from the Codex shared directory and deploys the stub beside them', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + await fse.ensureDir(path.join(homeDir, '.codex')); + const sharedLegacy = path.join(homeDir, '.agents/skills/team-wiki-codebase'); + await fse.ensureDir(sharedLegacy); + await fse.writeFile(path.join(sharedLegacy, 'SKILL.md'), WIKI_SKILL); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://example.test/team.git', + provider: 'git' as const, + reviewers: [], + sharing: { skills: {}, rules: { enforced: [] }, docs: { localDir: '' }, env: { injectShellProfile: true } }, + toolPaths: { codex: { skills: '.codex/skills' } }, + }; + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://example.test/team.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; + + const deployed = await deployBuiltinSkills(teamConfig, localConfig); + + expect(deployed).toBe(1); + expect(await fse.pathExists(sharedLegacy)).toBe(false); + // Codex reads .codex/skills; the shared .agents/skills is where its legacy + // copies live, and the prune is the only thing that reaches in there. + expect(await fse.readFile(path.join(homeDir, '.codex/skills/teamai/SKILL.md'), 'utf8')).toBe( + await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'), + ); + }); + + it('records every file the package still ships, so the prune keeps proving ownership', async () => { + const { PACKAGED_SKILL_FILES } = await import('../builtin-skills.js'); + + const shipped: string[] = []; + const walk = async (dir: string, prefix: string): Promise<void> => { + for (const entry of await fse.readdir(dir, { withFileTypes: true })) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) await walk(path.join(dir, entry.name), relative); + else shipped.push(relative); + } + }; + await walk(path.join(PACKAGE_ROOT, 'skills'), ''); + + // A packaged file missing from the manifest is one a later migration would + // leave behind on every machine, which no other test would notice. + for (const relative of shipped) { + const [skillName, ...rest] = relative.split('/'); + expect(PACKAGED_SKILL_FILES.get(skillName), relative).toContain(rest.join('/')); + } + }); + + it('removes the packaged files from a legacy directory but keeps what the member added', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://example.test/team.git', + provider: 'git' as const, + reviewers: [], + sharing: { skills: {}, rules: { enforced: [] }, docs: { localDir: '' }, env: { injectShellProfile: true } }, + toolPaths: { claude: { skills: '.claude/skills' } }, + }; + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://example.test/team.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; + + const wiki = path.join(homeDir, '.claude/skills/team-wiki-codebase'); + // What the release packaged… + for (const packaged of ['SKILL.md', 'README.md', 'references/methodology/phase0-collection.md', 'scripts/scan_repo.py']) { + await fse.ensureDir(path.join(wiki, path.dirname(packaged))); + await fse.writeFile(path.join(wiki, packaged), shipped('team-wiki-codebase', packaged)); + } + // …and what the member put beside it, which `overwrite: true` never deleted. + await fse.writeFile(path.join(wiki, 'references/methodology/my-notes.md'), '# mine'); + await fse.ensureDir(path.join(wiki, 'scripts/__pycache__')); + await fse.writeFile(path.join(wiki, 'scripts/__pycache__/scan_repo.cpython-311.pyc'), 'bytecode'); + + await deployBuiltinSkills(teamConfig, localConfig); + + expect(await fse.pathExists(path.join(wiki, 'SKILL.md'))).toBe(false); + expect(await fse.pathExists(path.join(wiki, 'README.md'))).toBe(false); + expect(await fse.pathExists(path.join(wiki, 'references/methodology/phase0-collection.md'))).toBe(false); + // Bytecode of a script we shipped is ours, so it does not keep the tree alive. + expect(await fse.pathExists(path.join(wiki, 'scripts'))).toBe(false); + // The member's file, and only it, survives. + expect(await fse.readFile(path.join(wiki, 'references/methodology/my-notes.md'), 'utf8')).toBe('# mine'); + }); + + it('keeps a file the member added beside the deployed stub', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://example.test/team.git', + provider: 'git' as const, + reviewers: [], + sharing: { skills: {}, rules: { enforced: [] }, docs: { localDir: '' }, env: { injectShellProfile: true } }, + toolPaths: { claude: { skills: '.claude/skills' } }, + }; + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://example.test/team.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + }; + + const stubDir = path.join(homeDir, '.claude/skills/teamai'); + await fse.ensureDir(path.join(stubDir, 'references')); + await fse.writeFile(path.join(stubDir, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.writeFile(path.join(stubDir, 'references/setup-admin.md'), shipped('teamai', 'references/setup-admin.md')); + await fse.writeFile(path.join(stubDir, 'references/team-playbook.md'), '# mine'); + + await deployBuiltinSkills(teamConfig, localConfig); + + expect(await fse.pathExists(path.join(stubDir, 'references/setup-admin.md'))).toBe(false); + expect(await fse.readFile(path.join(stubDir, 'references/team-playbook.md'), 'utf8')).toBe('# mine'); + expect(await fse.readFile(path.join(stubDir, 'SKILL.md'), 'utf8')).toBe( + await fse.readFile(path.join(PACKAGE_ROOT, 'skills/teamai/SKILL.md'), 'utf8'), + ); + }); + + it('leaves the Codex shared directory alone when another tool prunes and Codex is excluded', async () => { + const { deployBuiltinSkills } = await import('../builtin-skills.js'); + + await fse.ensureDir(path.join(homeDir, '.claude')); + await fse.ensureDir(path.join(homeDir, '.codex')); + const sharedLegacy = path.join(homeDir, '.agents/skills/team-wiki-codebase'); + await fse.ensureDir(sharedLegacy); + await fse.writeFile(path.join(sharedLegacy, 'SKILL.md'), WIKI_SKILL); + + const teamConfig = { + team: 'test', + description: '', + repo: 'https://example.test/team.git', + provider: 'git' as const, + reviewers: [], + sharing: { skills: {}, rules: { enforced: [] }, docs: { localDir: '' }, env: { injectShellProfile: true } }, + toolPaths: { claude: { skills: '.claude/skills' }, codex: { skills: '.codex/skills' } }, + }; + const localConfig = { + repo: { localPath: path.join(tmpDir, 'repo'), remote: 'https://example.test/team.git' }, + username: 'testuser', + updatePolicy: 'auto' as const, + additionalRoles: [], + scope: 'user' as const, + enabledAgents: ['claude'], + }; + + const deployed = await deployBuiltinSkills(teamConfig, localConfig); + + // .agents/skills is Codex's; the whitelist says Codex is neither written to + // nor deleted from, and Claude's pass must not reach it on Codex's behalf. + expect(deployed).toBe(1); + expect(await fse.pathExists(path.join(homeDir, '.claude/skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.readFile(path.join(sharedLegacy, 'SKILL.md'), 'utf8')).toBe(WIKI_SKILL); }); it('deploys a built-in Codex skill to its existing shared location', async () => { const { deployBuiltinSkills } = await import('../builtin-skills.js'); - const sharedSkill = path.join(homeDir, '.agents', 'skills', 'team-wiki-codebase'); + const sharedSkill = path.join(homeDir, '.agents', 'skills', 'teamai'); await fse.ensureDir(path.join(homeDir, '.codex')); await fse.ensureDir(sharedSkill); @@ -629,7 +1351,7 @@ describe('deployBuiltinSkills — skip uninstalled tools', () => { await deployBuiltinSkills(teamConfig, localConfig); expect(await fse.pathExists(path.join(sharedSkill, 'SKILL.md'))).toBe(true); - expect(await fse.pathExists(path.join(homeDir, '.codex', 'skills', 'team-wiki-codebase'))).toBe(false); + expect(await fse.pathExists(path.join(homeDir, '.codex', 'skills', 'teamai'))).toBe(false); }); }); @@ -686,8 +1408,8 @@ describe('deployBuiltinSkills — enabledAgents whitelist (#510)', () => { const deployed = await deployBuiltinSkills(teamConfig(), localConfig(['workbuddy'])); expect(deployed).toBeGreaterThan(0); - expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/team-wiki-codebase/SKILL.md'))).toBe(true); - expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/team-wiki-codebase'))).toBe(false); + expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/teamai'))).toBe(false); expect(await fse.pathExists(path.join(homeDir, '.hermes/skills'))).toBe(false); }); @@ -696,7 +1418,7 @@ describe('deployBuiltinSkills — enabledAgents whitelist (#510)', () => { const deployed = await deployBuiltinSkills(teamConfig(), localConfig()); expect(deployed).toBeGreaterThan(0); - expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/team-wiki-codebase/SKILL.md'))).toBe(true); - expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/team-wiki-codebase/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.workbuddy/skills/teamai/SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(homeDir, '.hermes/skills/teamai/SKILL.md'))).toBe(true); }); }); diff --git a/src/__tests__/team-wiki-codebase-skill.test.ts b/src/__tests__/team-wiki-codebase-skill.test.ts index df9184ca..58eab2fe 100644 --- a/src/__tests__/team-wiki-codebase-skill.test.ts +++ b/src/__tests__/team-wiki-codebase-skill.test.ts @@ -4,14 +4,18 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); -const SKILL_DIR = path.join(ROOT, 'skills', 'team-wiki-codebase'); +const SKILL_DIR = path.join(ROOT, 'skill-data', 'wiki'); const SKILL_FILES = [ path.join(SKILL_DIR, 'SKILL.md'), - path.join(SKILL_DIR, 'README.md'), + path.join(SKILL_DIR, 'references', 'overview.md'), + path.join(SKILL_DIR, 'references', 'phases', 'phase0-init.md'), path.join(SKILL_DIR, 'references', 'methodology', 'phase0-collection.md'), ] as const; +/** Phase 0's procedure moved out of SKILL.md into its own reference (#678). */ +const PHASE0_FILES = SKILL_FILES.filter((f) => !f.endsWith(path.join('wiki', 'SKILL.md'))); + const FORBIDDEN_REQUIRED_COMMANDS = [ 'team-wiki compile code', 'team-wiki reconcile', @@ -20,7 +24,7 @@ const FORBIDDEN_REQUIRED_COMMANDS = [ 'team-wiki refresh', ] as const; -describe('team-wiki-codebase builtin skill (issue #360 slice 1)', () => { +describe('wiki builtin skill content (issue #360 slice 1)', () => { it('ships the packaged skill files', () => { for (const file of SKILL_FILES) { expect(fs.existsSync(file), file).toBe(true); @@ -28,7 +32,7 @@ describe('team-wiki-codebase builtin skill (issue #360 slice 1)', () => { }); it('tells Phase 0 to run teamai codebase --extract', () => { - for (const file of SKILL_FILES) { + for (const file of PHASE0_FILES) { const text = fs.readFileSync(file, 'utf8'); expect(text, file).toContain('teamai codebase --extract'); } diff --git a/src/__tests__/uninstall.test.ts b/src/__tests__/uninstall.test.ts index 26cc0e4c..b8f320ca 100644 --- a/src/__tests__/uninstall.test.ts +++ b/src/__tests__/uninstall.test.ts @@ -2,9 +2,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import path from 'node:path'; import os from 'node:os'; import fse from 'fs-extra'; +import { fileURLToPath } from 'node:url'; +import { shipped, shippedSkillDigestsMock } from './helpers/shipped-skills.js'; + +const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); // ─── Mocks ───────────────────────────────────────────── +// A CLI-owned file is one whose content a release shipped; `shipped()` is it here. +vi.mock('../packaged-skill-digests.js', () => shippedSkillDigestsMock()); + const mockAutoDetectInit = vi.fn(); const mockSaveLocalConfig = vi.fn(); const mockSaveLocalConfigForScope = vi.fn(); @@ -120,10 +127,12 @@ async function setupFixture(tmpDir: string) { await fse.writeFile(path.join(homeDir, '.claude', 'rules', 'teamai-recall.md'), '# Recall Rule'); await fse.ensureDir(path.join(homeDir, '.claude', 'agents')); await fse.writeFile(path.join(homeDir, '.claude', 'agents', 'teamai-recall.md'), '# Recall Agent'); + await fse.ensureDir(path.join(homeDir, '.claude', 'skills', 'teamai')); + await fse.writeFile(path.join(homeDir, '.claude', 'skills', 'teamai', 'SKILL.md'), shipped('teamai', 'SKILL.md')); await fse.ensureDir(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings')); - await fse.writeFile(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings', 'SKILL.md'), '# Share Learnings'); + await fse.writeFile(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings', 'SKILL.md'), shipped('teamai-share-learnings', 'SKILL.md')); await fse.ensureDir(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase')); - await fse.writeFile(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase', 'SKILL.md'), '# Wiki Codebase'); + await fse.writeFile(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase', 'SKILL.md'), shipped('team-wiki-codebase', 'SKILL.md')); // Settings.json with hooks await fse.writeJson(path.join(homeDir, '.claude', 'settings.json'), { @@ -978,13 +987,213 @@ describe('uninstall', () => { expect(await fse.pathExists(path.join(homeDir, '.claude', 'agents', 'teamai-recall.md'))).toBe(false); expect(await fse.pathExists(path.join(homeDir, '.claude', 'rules', 'teamai-recall.md'))).toBe(false); expect(await fse.pathExists(codexRecallAgent)).toBe(false); - // Built-in skills removed + // Built-in skills removed: the deployed stub, and the directories earlier + // releases left behind. + expect(await fse.pathExists(path.join(homeDir, '.claude', 'skills', 'teamai'))).toBe(false); expect(await fse.pathExists(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings'))).toBe(false); expect(await fse.pathExists(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase'))).toBe(false); // User's own skill still preserved expect(await fse.pathExists(path.join(homeDir, '.claude', 'skills', 'my-own-skill'))).toBe(true); }); + it('removes only the packaged files from a CLI-owned skill dir, keeping what the member added', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + + // A machine that upgraded through the pre-stub releases: the packaged trees, + // with the member's own files mixed into them. Deployment never wrote those + // files and never deleted them, so uninstall must not either. + const skills = path.join(homeDir, '.claude', 'skills'); + const stub = path.join(skills, 'teamai'); + await fse.outputFile(path.join(stub, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.outputFile(path.join(stub, 'references', 'setup-admin.md'), shipped('teamai', 'references/setup-admin.md')); + await fse.outputFile(path.join(stub, 'references', 'team-playbook.md'), '# mine\n'); + + const legacy = path.join(skills, 'team-wiki-codebase'); + await fse.outputFile(path.join(legacy, 'SKILL.md'), shipped('team-wiki-codebase', 'SKILL.md')); + await fse.outputFile(path.join(legacy, 'scripts', 'scan_repo.py'), shipped('team-wiki-codebase', 'scripts/scan_repo.py')); + await fse.outputFile(path.join(legacy, 'references', 'methodology', 'my-notes.md'), '# mine\n'); + + // Nothing of the member's in this one, so it goes whole. + const legacyShare = path.join(skills, 'teamai-share-learnings'); + await fse.outputFile(path.join(legacyShare, 'SKILL.md'), shipped('teamai-share-learnings', 'SKILL.md')); + + const teamConfig = makeTeamConfig({ + toolPaths: { + claude: { + skills: '.claude/skills', + rules: '.claude/rules', + settings: '.claude/settings.json', + claudemd: '.claude/CLAUDE.md', + agents: '.claude/agents', + }, + }, + }); + const localConfig = makeLocalConfig(homeDir, repoPath); + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig }); + + await uninstall({ force: true }); + + // The member's files, and the directories holding them, survive. + expect(await fse.pathExists(path.join(stub, 'references', 'team-playbook.md'))).toBe(true); + expect(await fse.pathExists(path.join(legacy, 'references', 'methodology', 'my-notes.md'))).toBe(true); + // Everything TeamAI packaged is gone. + expect(await fse.pathExists(path.join(stub, 'SKILL.md'))).toBe(false); + expect(await fse.pathExists(path.join(stub, 'references', 'setup-admin.md'))).toBe(false); + expect(await fse.pathExists(path.join(legacy, 'SKILL.md'))).toBe(false); + expect(await fse.pathExists(path.join(legacy, 'scripts'))).toBe(false); + // A directory with nothing of the member's in it still goes whole. + expect(await fse.pathExists(legacyShare)).toBe(false); + }); + + it('names the file and the error when a packaged file cannot be deleted, instead of calling the directory kept', async () => { + if (process.getuid?.() === 0) return; // root ignores directory permissions + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + const stubDir = path.join(homeDir, '.claude', 'skills', 'teamai'); + await fse.chmod(stubDir, 0o555); + + const localConfig = makeLocalConfig(homeDir, repoPath); + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig: makeTeamConfig() }); + const { log } = await import('../utils/logger.js'); + try { + await uninstall({ force: true }); + } finally { + await fse.chmod(stubDir, 0o755); + } + + const warnings = (log.warn as ReturnType<typeof vi.fn>).mock.calls.map((c) => String(c[0])); + const about = warnings.filter((w) => w.includes(stubDir)); + expect(about).toHaveLength(1); + expect(about[0]).toContain('Could not delete packaged files under'); + expect(about[0]).toContain(path.join(stubDir, 'SKILL.md')); + expect(about[0]).not.toContain('did not put there'); + expect(await fse.pathExists(path.join(stubDir, 'SKILL.md'))).toBe(true); + }); + + it('removes the stub the installed CLI deployed, byte for byte, and keeps a same-named skill of the member\'s', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + const skills = path.join(homeDir, '.claude', 'skills'); + // What `teamai pull` writes now: the packaged stub, verbatim. + await fse.copy(path.join(PACKAGE_ROOT, 'skills', 'teamai', 'SKILL.md'), path.join(skills, 'teamai', 'SKILL.md')); + // A skill the member wrote under a legacy name: no content a release shipped. + await fse.outputFile(path.join(skills, 'team-wiki-codebase', 'SKILL.md'), '---\nname: team-wiki-codebase\n---\n# mine\n'); + + mockAutoDetectInit.mockResolvedValue({ localConfig: makeLocalConfig(homeDir, repoPath), teamConfig: makeTeamConfig() }); + await uninstall({ force: true }); + + expect(await fse.pathExists(path.join(skills, 'teamai'))).toBe(false); + expect(await fse.readFile(path.join(skills, 'team-wiki-codebase', 'SKILL.md'), 'utf8')).toContain('# mine'); + }); + + it('does not delete through a linked skills root, the same line pull stops at', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + // The member's dotfiles checkout, linked in as ~/.claude/skills. + const dotfiles = path.join(tmpDir, 'dotfiles-skills'); + await fse.move(path.join(homeDir, '.claude', 'skills'), dotfiles); + await fse.symlink(dotfiles, path.join(homeDir, '.claude', 'skills'), 'dir'); + + const localConfig = makeLocalConfig(homeDir, repoPath); + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig: makeTeamConfig() }); + await uninstall({ force: true }); + + expect(await fse.pathExists(path.join(dotfiles, 'teamai', 'SKILL.md'))).toBe(true); + }); + + it('does not delete through a linked agent directory either, a link higher up the path', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + const dotfiles = path.join(tmpDir, 'dotfiles-claude'); + await fse.move(path.join(homeDir, '.claude'), dotfiles); + await fse.symlink(dotfiles, path.join(homeDir, '.claude'), 'dir'); + + const localConfig = makeLocalConfig(homeDir, repoPath); + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig: makeTeamConfig() }); + await uninstall({ force: true }); + + expect(await fse.pathExists(path.join(dotfiles, 'skills', 'teamai', 'SKILL.md'))).toBe(true); + }); + + it('does not delete through a linked COPILOT_HOME, Copilot\'s own base directory in user scope', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + const target = path.join(tmpDir, 'dotfiles-copilot'); + await fse.ensureDir(path.join(target, 'skills', 'teamai')); + await fse.writeFile(path.join(target, 'skills', 'teamai', 'SKILL.md'), shipped('teamai', 'SKILL.md')); + await fse.symlink(target, path.join(homeDir, '.copilot'), 'dir'); + + const localConfig = makeLocalConfig(homeDir, repoPath); + const teamConfig = makeTeamConfig(); + teamConfig.toolPaths.copilot = { skills: '.github/skills', userScope: { skills: 'skills' } }; + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig }); + await uninstall({ force: true }); + + expect(await fse.pathExists(path.join(target, 'skills', 'teamai', 'SKILL.md'))).toBe(true); + }); + + it('removes the stub where Hermes keeps skills, under HERMES_HOME outside the home directory', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + const hermesHome = path.join(tmpDir, 'elsewhere', 'hermes'); + vi.stubEnv('HERMES_HOME', hermesHome); + const stub = path.join(hermesHome, 'skills', 'teamai', 'SKILL.md'); + await fse.ensureDir(path.dirname(stub)); + await fse.writeFile(stub, shipped('teamai', 'SKILL.md')); + + const localConfig = makeLocalConfig(homeDir, repoPath); + const teamConfig = makeTeamConfig(); + teamConfig.toolPaths.hermes = { skills: '.hermes/skills' }; + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig }); + await uninstall({ force: true }); + + expect(await fse.pathExists(stub)).toBe(false); + }); + + it('removes the stub Codex kept in the shared .agents/skills root, and nothing else there', async () => { + const { homeDir, repoPath } = await setupFixture(tmpDir); + vi.stubEnv('HOME', homeDir); + vi.stubEnv('SHELL', '/bin/zsh'); + + // resolveSkillDestination writes Codex's copy here whenever the skill + // already lives in the shared root, so this is where the stub ends up on a + // machine that has ever had one. + const sharedStub = path.join(homeDir, '.agents', 'skills', 'teamai'); + await fse.ensureDir(sharedStub); + await fse.writeFile(path.join(sharedStub, 'SKILL.md'), shipped('teamai', 'SKILL.md')); + const sharedUserSkill = path.join(homeDir, '.agents', 'skills', 'my-own-skill'); + await fse.ensureDir(sharedUserSkill); + await fse.writeFile(path.join(sharedUserSkill, 'SKILL.md'), '# Mine\n'); + + const teamConfig = makeTeamConfig({ + toolPaths: { + claude: { + skills: '.claude/skills', + rules: '.claude/rules', + settings: '.claude/settings.json', + claudemd: '.claude/CLAUDE.md', + agents: '.claude/agents', + }, + codex: { skills: '.codex/skills' }, + }, + }); + const localConfig = makeLocalConfig(homeDir, repoPath); + mockAutoDetectInit.mockResolvedValue({ localConfig, teamConfig }); + + await uninstall({ force: true }); + + expect(await fse.pathExists(sharedStub)).toBe(false); + expect(await fse.pathExists(sharedUserSkill)).toBe(true); + }); + it('清理 CLAUDE.md 中所有 teamai section(culture/claudemd/recall-rules)', async () => { const { homeDir, repoPath } = await setupFixture(tmpDir); vi.stubEnv('HOME', homeDir); @@ -1329,6 +1538,7 @@ describe('uninstall', () => { }); // Remove all teamai resources from claude so it has zero teamai presence await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-skill')); + await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase')); await fse.remove(path.join(homeDir, '.claude', 'rules', 'team-rule.md')); @@ -1393,6 +1603,7 @@ describe('uninstall', () => { hooks: { SessionStart: [{ matcher: '*', hooks: [{ type: 'command', command: 'echo hi' }] }] }, }); await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-skill')); + await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase')); await fse.remove(path.join(homeDir, '.claude', 'rules', 'team-rule.md')); @@ -1447,6 +1658,7 @@ describe('uninstall', () => { }); // Also remove all other teamai resources so nothing triggers cleanup. await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-skill')); + await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'teamai-share-learnings')); await fse.remove(path.join(homeDir, '.claude', 'skills', 'team-wiki-codebase')); await fse.remove(path.join(homeDir, '.claude', 'rules', 'team-rule.md')); diff --git a/src/agent-skills.ts b/src/agent-skills.ts index 71620dc5..b3bfa07a 100644 --- a/src/agent-skills.ts +++ b/src/agent-skills.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { listDirs, pathExists, readFileSafe } from './utils/fs.js'; import { detectInstalledAgents, type ResolvedAgent } from './known-agents.js'; -import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; +import { isCliOwnedSkillName } from './builtin-skills.js'; import type { LocalConfig, TeamaiConfig } from './types.js'; import { getUserHome } from './utils/home.js'; import { parseFrontmatter } from './utils/frontmatter.js'; @@ -107,7 +107,9 @@ async function collectTeamRepoSkills(repoPath: string): Promise<Map<string, { na /** Resolve a skill name to its source tag using the prebuilt context. */ export function classifySkill(name: string, ctx: ClassifyContext): SkillSource { - if (BUILTIN_SKILL_NAMES.has(name)) return { kind: 'builtin' }; + // Same rule as the push scan and uninstall: a name a pre-stub release deployed + // is ours until the next pull prunes it, not a member's local-only skill. + if (isCliOwnedSkillName(name)) return { kind: 'builtin' }; if (ctx.teamSkills.has(name)) { return { kind: 'team', namespace: ctx.teamSkills.get(name)?.namespace }; } diff --git a/src/builtin-skills.ts b/src/builtin-skills.ts index 136b641d..99dcdbec 100644 --- a/src/builtin-skills.ts +++ b/src/builtin-skills.ts @@ -1,84 +1,513 @@ import fs from 'node:fs'; +import { createHash } from 'node:crypto'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import fse from 'fs-extra'; -import { pathExists } from './utils/fs.js'; +import { pathExists, remove } from './utils/fs.js'; import { log } from './utils/logger.js'; import type { TeamaiConfig, LocalConfig } from './types.js'; -import { resolveToolBaseDir, isAgentExcluded, scopedToolPaths } from './types.js'; -import { isToolInstalledForConfig, ResourceHandler } from './resources/base.js'; -import { ensureSkillFrontmatter, resolveSkillDestination } from './resources/skills.js'; +import { resolveBaseDir, isAgentExcluded, scopedToolPaths } from './types.js'; +import { ResourceHandler } from './resources/base.js'; +import { CODEX_TOOL, resolveSkillDestination, SHARED_AGENT_SKILLS_PATH, skillsDirForTool, skillTargetForTool } from './resources/skills.js'; import { getUserHome } from './utils/home.js'; +import { packagedSkillRoots } from './skill-content.js'; +import { PACKAGED_SKILL_DIGESTS } from './packaged-skill-digests.js'; // ─── Built-in skills deployment ────────────────────────── // -// CLI ships with built-in skills (e.g. teamai-contribute). -// These are bundled in the npm package under skills/. -// On each `teamai pull`, we copy them to local AI tool -// skill directories so they're always available and -// stay in sync with the CLI version. +// The CLI ships one deployable skill: the `teamai` discovery +// stub under skills/. On each `teamai pull` its SKILL.md is +// copied to local AI tool skill directories. The workflow +// content it points at is never copied — it lives under +// skill-data/ and is printed by `teamai skill get`, so what +// the agent reads always matches the installed CLI version. // // npm package -// skills/teamai-contribute/SKILL.md +// skills/teamai/SKILL.md (about 2 KB) // │ // ▼ (teamai pull / teamai init) -// ~/.claude/skills/teamai-contribute/SKILL.md -// ~/.claude-internal/skills/teamai-contribute/SKILL.md -// ~/.codex-internal/skills/teamai-contribute/SKILL.md -// ~/.cursor/skills/teamai-contribute/SKILL.md +// ~/.claude/skills/teamai/SKILL.md +// ~/.codex-internal/skills/teamai/SKILL.md +// ~/.cursor/skills/teamai/SKILL.md // ... // +// skill-data/{core,setup,wiki,share}/ never copied +// + +/** + * Names of CLI built-in skills. Used by push to exclude them from team repo + * push, by pull cleanup, and by uninstall. + */ +export const BUILTIN_SKILL_NAMES = new Set(['teamai']); + +/** + * Built-in skill directories earlier releases deployed, kept only so that pull + * can remove them from agent skills directories. Retire this set once 0.25.x, + * the last release to deploy them, is no longer in the field. + * + * Only names the CLI actually wrote belong here. `teamai-workflow` and + * `teamai-import` were reserved in the old BUILTIN_SKILL_NAMES guard but never + * packaged, so a directory by either name is a user's own skill and must not be + * removed. + */ +export const LEGACY_BUILTIN_SKILL_NAMES = new Set([ + 'teamai-share-learnings', + 'team-wiki-codebase', +]); /** - * Get the path to the built-in skills directory bundled with the CLI. - * Resolves relative to the dist/ directory where the compiled CLI lives. + * The legacy directory that depended on recall. `teamai recall disable` still + * removes it, as it did before the stub, so a member who upgrades and disables + * recall before their next pull is not left with the old share workflow. */ -function getBuiltinSkillsDir(): string { - // __dirname equivalent for ESM: import.meta.url → file path → parent - const distDir = path.dirname(fileURLToPath(import.meta.url)); - // skills/ is at package root, dist/ is one level down - return path.join(distDir, '..', 'skills'); +export const LEGACY_RECALL_SKILL_NAMES = new Set(['teamai-share-learnings']); + +/** + * Whether a skill directory by this name is the CLI's, current or legacy, and + * therefore never a user's own to push. A member who runs `teamai push --all` + * after upgrading but before pulling still has the legacy trees on disk. + */ +export function isCliOwnedSkillName(name: string): boolean { + return BUILTIN_SKILL_NAMES.has(name) || LEGACY_BUILTIN_SKILL_NAMES.has(name); } -/** Names of CLI built-in skills. Used by push to exclude them from team repo push. */ -export const BUILTIN_SKILL_NAMES = new Set(['teamai-share-learnings', 'team-wiki-codebase', 'teamai-workflow', 'teamai-import']); +/** + * Every file a release ever packaged under `skills/`, by directory name: the + * paths of PACKAGED_SKILL_DIGESTS. A path alone does not make a file ours — + * `removeOwnedFiles` also needs its content to match a shipped version — but + * the list is what the deploy prune reads to find paths the current package no + * longer ships. + * + * `teamai-wiki` (0.13.0, 0.16.x) is deliberately absent: it predates the trees + * this migration is about, and widening a destructive set is its own change. + */ +export const PACKAGED_SKILL_FILES: ReadonlyMap<string, readonly string[]> = new Map( + [...PACKAGED_SKILL_DIGESTS].map(([skill, files]) => [skill, [...files.keys()]]), +); /** - * Built-in skills that depend on recall being enabled. Skipped when recall is disabled. + * The digest PACKAGED_SKILL_DIGESTS records for a file: sha256 of its bytes. + * The whole file, frontmatter included: a member who changed only a skill's + * name, description or allowed-tools changed the skill, and it is theirs. + */ +export function packagedSkillDigest(content: Buffer): string { + return createHash('sha256').update(content).digest('hex'); +} + +/** Digests by path: what `removeOwnedFiles` may remove, and only at that content. */ +export type OwnedSkillFiles = ReadonlyMap<string, ReadonlySet<string>>; + +/** + * The files of `skillName` the CLI provably wrote — every version a release + * shipped, plus, for the stub, the one this package ships now — optionally + * narrowed to `paths`. + */ +export async function ownedSkillFiles(skillName: string, paths?: readonly string[]): Promise<OwnedSkillFiles> { + const owned = new Map<string, Set<string>>(); + for (const [relative, digests] of PACKAGED_SKILL_DIGESTS.get(skillName) ?? []) { + if (!paths || paths.includes(relative)) owned.set(relative, new Set(digests)); + } + if (BUILTIN_SKILL_NAMES.has(skillName) && (!paths || paths.includes('SKILL.md'))) { + const stubPath = path.join(packagedSkillRoots().deployRoot, skillName, 'SKILL.md'); + if (await pathExists(stubPath)) { + const digests = owned.get('SKILL.md') ?? new Set<string>(); + digests.add(packagedSkillDigest(await fs.promises.readFile(stubPath))); + owned.set('SKILL.md', digests); + } + } + return owned; +} + +/** True when `file` sits at an owned path with content a release shipped there. */ +async function isOwnedFile(file: string, relative: string, owned: OwnedSkillFiles): Promise<boolean> { + const digests = owned.get(relative); + if (!digests) return false; + return digests.has(packagedSkillDigest(await fs.promises.readFile(file))); +} + +/** + * Python bytecode cache of a script we shipped: `a/__pycache__/x.cpython-311.pyc` + * for an `a/x.py` present and proven ours by content. Compiler output of our + * own file, so it carries nothing a member wrote and does not make a directory + * theirs. Bytecode beside a member's edit of the script, or with no script at + * all, is theirs. + */ +function isDerivedArtifact(relativePath: string, provenScripts: ReadonlySet<string>): boolean { + const parts = relativePath.split('/'); + if (parts.length < 2 || parts[parts.length - 2] !== '__pycache__' || !relativePath.endsWith('.pyc')) return false; + const stem = parts[parts.length - 1].split('.')[0]; + return provenScripts.has([...parts.slice(0, -2), `${stem}.py`].join('/')); +} + +/** + * Every file under `dir`, as paths relative to it. Symlinks count as files. + * Not the shared walker in utils/fs: that one skips `__pycache__`, and the prune + * has to see it to decide whether a directory is empty of the member's files. + */ +async function walkFiles(dir: string, prefix = ''): Promise<string[]> { + const found: string[] = []; + for (const entry of await fs.promises.readdir(dir, { withFileTypes: true })) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + found.push(...await walkFiles(path.join(dir, entry.name), relative)); + } else { + found.push(relative); + } + } + return found; +} + +/** + * Remove `dir` and every directory under it that holds nothing. A directory + * that still has something in it stays, which is the point: that something is + * the member's. Any other failure (permissions, a busy mount) is returned, so + * the prune does not report a directory gone that is still there. + */ +async function removeEmptyDirs(dir: string): Promise<{ file: string; error: string }[]> { + const failures: { file: string; error: string }[] = []; + let entries; + try { + entries = await fs.promises.readdir(dir, { withFileTypes: true }); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'ENOENT') failures.push({ file: dir, error: (e as Error).message }); + return failures; + } + for (const entry of entries) { + if (entry.isDirectory()) failures.push(...await removeEmptyDirs(path.join(dir, entry.name))); + } + try { + await fs.promises.rmdir(dir); + } catch (e) { + // Not empty is the expected outcome for a directory holding the member's + // files — and some platforms say EACCES for that under a read-only parent, + // so the directory's contents, not the error code, decide. + const code = (e as NodeJS.ErrnoException).code; + const stillHolds = code === 'ENOTEMPTY' || code === 'EEXIST' + || (await fs.promises.readdir(dir).catch(() => [])).length > 0; + if (!stillHolds) failures.push({ file: dir, error: (e as Error).message }); + } + return failures; +} + +/** What `removeOwnedFiles` did and did not do, for the caller to report. */ +export interface PruneResult { + /** True when a link sits between the base and the skill directory: nothing was touched. */ + skippedSymlink: boolean; + /** Files left in place because the member, not the CLI, put them there. */ + foreign: number; + /** Files left in place because their backup could not be written, and why. */ + unbackedUp: { file: string; error: string }[]; + /** Files archived but not deleted, leaving the tree half-pruned, and why. */ + notRemoved: { file: string; error: string }[]; + /** Whether anything was actually copied, so a log only names a backup that exists. */ + backedUp: number; +} + +/** True when the directory is gone: nothing of the member's, nothing unsaved. */ +export function prunedWhole(result: PruneResult): boolean { + return !result.skippedSymlink + && result.foreign === 0 + && result.unbackedUp.length === 0 + && result.notRemoved.length === 0; +} + +/** + * Remove from `dir` the files the CLI put there, then the directories that end + * up empty. Anything that stopped it — a member's own file, a failed backup, a + * failed delete, a link — is in the result, so the caller can say which. * - * Only teamai-share-learnings belongs here: it contributes learnings back to the - * team repo, which is meaningful only when recall is on. team-wiki-codebase is a - * knowledge-base generator and does not depend on recall, so it must always deploy. + * Exported because uninstall must delete a CLI-owned skill directory by the same + * rule pull does: a file a member added beside our packaged ones was never ours + * to write and is not ours to remove, whichever command is doing the removing. */ -export const RECALL_DEPENDENT_SKILLS = new Set(['teamai-share-learnings']); +export async function removeOwnedFiles( + dir: string, + owned: OwnedSkillFiles, + baseDir: string, + backupDir?: string, +): Promise<PruneResult> { + const result: PruneResult = { + skippedSymlink: false, foreign: 0, unbackedUp: [], notRemoved: [], backedUp: 0, + }; + + // A link anywhere between the base directory and this one points at files we + // never wrote — a shared checkout, a dotfiles repo. `readdir` follows it and + // every path under it matches ours by name, so the walk would delete someone + // else's files through the link. Ownership stops at the first link. + if (await crossesSymlink(baseDir, dir)) { + result.skippedSymlink = true; + return result; + } + + let entries: string[]; + try { + entries = await walkFiles(dir); + } catch (e) { + // An unreadable subdirectory. Reporting it is the point: a silent return + // leaves the tree in place while `pull` says it succeeded. + result.notRemoved.push({ file: dir, error: (e as Error).message }); + return result; + } + + // Decide ownership before removing anything: bytecode is ours only beside a + // script proven ours, and that proof has to be taken while the script is + // still there. + const proven = new Set<string>(); + for (const relative of entries) { + const file = path.join(dir, relative); + try { + // Ours only at a path a release shipped *and* with content one of them + // shipped there. A member's edit, or a skill of their own that uses a + // packaged name under a root TeamAI never managed, fails the second test + // and stays, with its directory. No release shipped a symlink. + if (!(await fs.promises.lstat(file)).isSymbolicLink() && await isOwnedFile(file, relative, owned)) proven.add(relative); + } catch (e) { + // Unreadable, so unprovable: kept, and said so. + result.notRemoved.push({ file, error: (e as Error).message }); + } + } -async function copyBuiltinSkillDir(srcDir: string, destDir: string): Promise<void> { - await fse.copy(srcDir, destDir, { - overwrite: true, - filter: (srcPath: string) => !path.basename(srcPath).startsWith('.'), - }); + for (const relative of entries) { + const file = path.join(dir, relative); + if (result.notRemoved.some((failure) => failure.file === file)) continue; + const owns = proven.has(relative) + || (isDerivedArtifact(relative, proven) && !(await fs.promises.lstat(file)).isSymbolicLink()); + if (!owns) { + result.foreign++; + continue; + } + // Content proves the CLI wrote the file; a copy still goes to the archive + // before the delete, so no removal is a one-way door. + if (backupDir) { + try { + // `errorOnExist` turns a colliding path into a failure rather than a + // silent overwrite: a lost copy would be the data loss this exists to + // prevent, wearing the log line of a success. + await fse.copy(file, path.join(backupDir, relative), { overwrite: false, errorOnExist: true }); + result.backedUp++; + } catch (e) { + // A full disk, a read-only home, a colliding copy. Keep the file: a + // backup that did not happen must not authorise the delete. + result.unbackedUp.push({ file, error: (e as Error).message }); + continue; + } + } + try { + await remove(file); + } catch (e) { + // A read-only parent. The archive holds the copy, but the original stays, + // so the tree is half-pruned: say so rather than let a debug line carry it. + result.notRemoved.push({ file, error: (e as Error).message }); + } + } + result.notRemoved.push(...await removeEmptyDirs(dir)); + + return result; +} + +/** + * True when any path component between `baseDir` and `target` is a symlink, or + * `target` is not under `baseDir` at all. + * + * Checking `target` alone is not enough: a member who links `~/.claude/skills` + * — or `~/.config/opencode`, or `~/.claude` itself — at a dotfiles checkout + * leaves every skill directory under it a real directory, so `lstat` on one + * says nothing. Components at or above `baseDir` are not checked: a home + * directory that itself sits under a link is ordinary, and refusing there would + * disable deployment for those machines. + */ +async function crossesSymlink(baseDir: string, target: string): Promise<boolean> { + const relative = path.relative(baseDir, target); + if (relative.startsWith('..') || path.isAbsolute(relative)) return true; + + let walked = baseDir; + for (const segment of relative.split(path.sep).filter(Boolean)) { + walked = path.join(walked, segment); + try { + if ((await fs.promises.lstat(walked)).isSymbolicLink()) return true; + } catch { + return false; // does not exist yet: nothing to walk through + } + } + return false; +} + +/** + * One backup root per process run. A date alone collides: two pulls on the same + * day would have the second overwrite the first's copies. + */ +const PRUNE_RUN_ID = `${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`; + +/** + * Where the prune parks what it removes: outside every agent directory, so no + * agent reads it back as a skill, and under the member's own `~/.teamai`. + * + * The skill root is part of the path because one tool can prune the same skill + * name from two roots — Codex reads `.codex/skills` and the shared + * `.agents/skills` — and those two copies are different files. + */ +function skillBackupDir(baseDir: string, tool: string, skillRoot: string, skillName: string): string { + const rootSlug = skillRoot.replace(/[\\/:]+/g, '-').replace(/^-+/, ''); + // `inheritUserScope` deploys to the user base and then the project base in one + // process, with the same tool, root and skill name. Without the base in the + // path the second pass collides with the first, and `errorOnExist` turns that + // into files it can neither archive nor prune. + const baseSlug = `${path.basename(baseDir) || 'root'}-${createHash('sha256').update(baseDir).digest('hex').slice(0, 8)}`; + // Machine data, not project data: under project scope `baseDir` is the repo + // root, where a backup per session start would show up as a dirty tree. + return path.join(getUserHome(), '.teamai', 'removed-skills', PRUNE_RUN_ID, baseSlug, tool, rootSlug, skillName); +} + +/** Where a tool keeps its skills on this machine, and where the link guard starts. */ +export interface BuiltinSkillsTarget { + skillsDir: string; + /** + * The scope root (home, or the project root) when the skills directory sits + * under it, else the parent of the configured root: `COPILOT_HOME`, + * `HERMES_HOME` and OpenClaw's workspace can live anywhere, and the guard + * must still check that root and every component below it. + */ + guardBase: string; +} + +/** + * The base the link guard walks down from: the scope root — home, or the + * project root — when `skillsDir` sits under it, since a link at or above that + * is ordinary. Not the tool's base directory: for Copilot that is + * `COPILOT_HOME`, and starting there would never check whether `COPILOT_HOME` + * itself is a link. A root configured outside the scope root (`HERMES_HOME`, + * an OpenClaw workspace) has the walk start just above it, so that root is + * checked too. + */ +export function skillsGuardBase(scopeRoot: string, skillsDir: string): string { + const relative = path.relative(scopeRoot, skillsDir); + const underRoot = relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative); + return underRoot ? scopeRoot : path.dirname(path.dirname(skillsDir)); +} + +/** + * The skills directory `tool` receives built-ins into, or null when it cannot + * receive them. The same resolver team-skill sync uses (`skillsDirForTool`), + * so the stub lands where every other skill does — OpenClaw's workspace, + * `HERMES_HOME` — and the prune looks where earlier releases wrote. + */ +export async function builtinSkillsTarget( + tool: string, + configuredSkillsPath: string, + localConfig?: LocalConfig, +): Promise<BuiltinSkillsTarget | null> { + if (!localConfig) { + const baseDir = getUserHome(); + if (!await ResourceHandler.isToolInstalled(configuredSkillsPath, baseDir)) return null; + return { skillsDir: path.join(baseDir, configuredSkillsPath), guardBase: baseDir }; + } + const skillsDir = await skillsDirForTool(tool, configuredSkillsPath, localConfig); + if (skillsDir === null) return null; + return { skillsDir, guardBase: skillsGuardBase(resolveBaseDir(localConfig), skillsDir) }; +} + +/** + * Remove the skill directories earlier releases deployed. + * + * Only the files those releases packaged: each was overwritten on every pull + * (`overwrite: true`), so no local edit ever survived in one, while a file the + * member added beside them was never touched and is not ours to delete. A + * directory that still holds such a file is kept, and the member is told. + */ +export async function pruneLegacyBuiltinSkills( + tool: string, + { skillsDir, guardBase }: BuiltinSkillsTarget, + names: ReadonlySet<string> = LEGACY_BUILTIN_SKILL_NAMES, +): Promise<void> { + // The shared .agents/skills directory belongs to Codex alone. Reaching it from + // another tool's pass would delete Codex's copies while Codex is excluded or + // not installed, which the enabledAgents whitelist rules out. + const skillRoots = [skillsDir]; + if (tool === CODEX_TOOL) skillRoots.push(path.join(guardBase, SHARED_AGENT_SKILLS_PATH)); + for (const legacyName of names) { + for (const root of skillRoots) { + const dir = path.join(root, legacyName); + if (!await pathExists(dir)) continue; + try { + const backupDir = skillBackupDir(guardBase, tool, path.relative(guardBase, root), legacyName); + const result = await removeOwnedFiles(dir, await ownedSkillFiles(legacyName), guardBase, backupDir); + const saved = result.backedUp > 0 ? `; a copy is in ${backupDir}` : ''; + if (prunedWhole(result)) { + log.debug(`Removed legacy built-in skill ${legacyName} from ${tool} (${dir})${saved}`); + } else if (result.skippedSymlink) { + // Never the "delete the rest yourself" sentence here: following it + // would destroy exactly what the guard just protected. + log.warn(`Skipped "${legacyName}" (${tool}): ${dir} is reached through a symlink, so TeamAI left it alone. Nothing was read, copied or removed.`); + } else if (result.unbackedUp.length > 0) { + log.warn(`Kept "${legacyName}" (${tool}): ${result.unbackedUp.length} file(s) in ${dir} could not be backed up, so they were not removed. First: ${result.unbackedUp[0].file} — ${result.unbackedUp[0].error}`); + } else if (result.notRemoved.length > 0) { + log.warn(`Partly removed "${legacyName}" (${tool}): ${result.notRemoved.length} file(s) in ${dir} were archived but could not be deleted. First: ${result.notRemoved[0].file} — ${result.notRemoved[0].error}`); + } else { + log.warn(`Kept "${legacyName}" (${tool}): ${dir} holds files TeamAI did not put there. The packaged files were removed${saved}; delete the rest yourself once you have saved what you need.`); + } + } catch (e) { + log.debug(`Could not remove legacy built-in skill ${legacyName} from ${tool}: ${(e as Error).message}`); + } + } + } +} + +/** + * Codex reads both `.codex/skills` and the shared `.agents/skills`, and the + * destination resolver picks the shared copy whenever one exists. The stub has + * just been written to one of them; a copy an earlier release left in the other + * would keep its old SKILL.md and references beside it, so Codex would see two + * `teamai` skills and one of them stale. That copy goes, by the same ownership + * rule as the rest: only files whose content a release shipped, archived first. + */ +async function retireOtherCodexCopy( + tool: string, + skillName: string, + deployedDir: string, + { skillsDir, guardBase }: BuiltinSkillsTarget, +): Promise<void> { + const candidates = [path.join(skillsDir, skillName), path.join(guardBase, SHARED_AGENT_SKILLS_PATH, skillName)]; + for (const other of candidates) { + if (path.resolve(other) === path.resolve(deployedDir) || !await pathExists(other)) continue; + const backupDir = skillBackupDir(guardBase, tool, path.relative(guardBase, path.dirname(other)), skillName); + const result = await removeOwnedFiles(other, await ownedSkillFiles(skillName), guardBase, backupDir); + if (prunedWhole(result)) { + log.debug(`Removed the second Codex copy of ${skillName} at ${other}; the stub is at ${deployedDir}`); + } else if (result.skippedSymlink) { + log.warn(`Kept ${other}: it is reached through a symlink, so TeamAI left it alone. Codex also reads the stub at ${deployedDir}.`); + } else if (result.unbackedUp.length > 0) { + log.warn(`Kept ${result.unbackedUp.length} file(s) in ${other}: their backup could not be written, so they were not removed. First: ${result.unbackedUp[0].file} — ${result.unbackedUp[0].error}`); + } else if (result.notRemoved.length > 0) { + log.warn(`Could not finish removing ${other}: ${result.notRemoved.length} file(s) or directories stayed. First: ${result.notRemoved[0].file} — ${result.notRemoved[0].error}`); + } else { + log.warn(`Kept ${other}: it holds files TeamAI did not write, so Codex sees it beside the stub at ${deployedDir}. Remove it once you have saved what you need.`); + } + } } /** * Deploy CLI built-in skills to all configured AI tool skill directories. * - * Copies each skill directory from the npm package's skills/ folder - * to every tool's skills path defined in teamai.yaml. + * Copies the SKILL.md of each skill in the npm package's skills/ folder to + * every tool's skills path defined in teamai.yaml. Only that one file: the + * deployed unit is a discovery stub, and its workflow content is served by + * `teamai skill get` from skill-data/. + * + * The stub is written verbatim — no frontmatter repair on the way out, so a + * deployed copy that differs from the packaged one is a bug, not a variant. + * + * Reporting-only HTTP teams get the stub too. The release before this one had + * nothing to deploy there that worked without a team repo, so it deployed + * nothing; the stub's content is served by the installed CLI, and `skill get + * wiki` — a local knowledge-base generator — needs no repo at all. Skipping it + * while still pruning the legacy trees would leave those members with no + * discoverable entry point at all. * * Silently skips if: * - Built-in skills directory doesn't exist (dev environment without build) * - A tool's skills directory is not configured */ -export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig, options?: { reportingOnly?: boolean; skipRecall?: boolean }): Promise<number> { - // Reporting-only HTTP mode has no team repo to write to, so the team-repo- - // dependent built-in skill (teamai-share-learnings) is non-functional there. - // Skip built-in skills entirely. - if (options?.reportingOnly) { - log.debug('Reporting-only mode (no team repo): skipping built-in skills (teamai-share-learnings)'); - return 0; - } - - const builtinDir = getBuiltinSkillsDir(); +export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig): Promise<number> { + const builtinDir = packagedSkillRoots().deployRoot; if (!await pathExists(builtinDir)) { log.debug('No built-in skills directory found, skipping deployment'); @@ -95,7 +524,6 @@ export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig? // Filter to directories that contain SKILL.md const skillNames: string[] = []; for (const entry of entries) { - if (options?.skipRecall && RECALL_DEPENDENT_SKILLS.has(entry)) continue; const skillMd = path.join(builtinDir, entry, 'SKILL.md'); if (await pathExists(skillMd)) { skillNames.push(entry); @@ -104,38 +532,79 @@ export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig? if (skillNames.length === 0) return 0; - const defaultBaseDir = getUserHome(); let deployed = 0; for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig ?? {}))) { if (!toolPath.skills) continue; - const baseDir = localConfig ? resolveToolBaseDir(tool, localConfig) : defaultBaseDir; - // Skip tools that are not installed - const installed = localConfig - ? await isToolInstalledForConfig(tool, toolPath.skills, localConfig) - : await ResourceHandler.isToolInstalled(toolPath.skills, baseDir); - if (!installed) { + // Skip tools that cannot receive skills: not installed, no workspace. + const target = await builtinSkillsTarget(tool, toolPath.skills, localConfig); + if (!target) { log.debug(`Skipping built-in skill deployment for ${tool}: tool not installed`); continue; } + const baseDir = target.guardBase; + // An excluded agent is neither written to nor deleted from (usage-guide: + // "the enabledAgents whitelist also gates CLI built-in skills"), so its + // legacy directories are left alone too. if (localConfig && isAgentExcluded(localConfig, tool)) continue; + let deployedHere = 0; for (const skillName of skillNames) { const srcDir = path.join(builtinDir, skillName); - const destDir = await resolveSkillDestination(tool, toolPath.skills, baseDir, skillName, srcDir); + // Resolved without a source path, so the resolver only answers where the + // skill lives and touches nothing: its Codex reconciliation deletes a + // duplicate, and nothing may be deleted before the link guard has run. + // The other Codex copy is dealt with below, under the guard. + const destDir = localConfig + ? await skillTargetForTool(tool, toolPath.skills, localConfig, skillName) ?? path.join(target.skillsDir, skillName) + : await resolveSkillDestination(tool, toolPath.skills, baseDir, skillName); try { - await copyBuiltinSkillDir(srcDir, destDir); - - // Ensure SKILL.md has proper YAML frontmatter (name + description) - await ensureSkillFrontmatter(destDir, skillName); + // A symlinked destination points somewhere we do not own. Writing + // through it would put the stub outside the agent directory, which is + // the same reason the prune refuses to walk it. Neither step runs. + if (await crossesSymlink(baseDir, destDir)) { + log.warn(`Skipped ${skillName} (${tool}): ${destDir} is reached through a symlink, and TeamAI does not write through one. Remove the link to let the skill deploy.`); + continue; + } + // The stub first: if it cannot be written, the pre-stub SKILL.md and the + // references it points at stay together, a working old skill rather + // than an old skill whose references are gone. + await fse.ensureDir(destDir); + await fse.copy(path.join(srcDir, 'SKILL.md'), path.join(destDir, 'SKILL.md'), { overwrite: true }); + // Releases before the discovery stub deployed this same directory with a + // references/ tree beside SKILL.md, ~39 KB of pre-stub instructions the + // new SKILL.md no longer points at. The files those releases wrote go — + // only the paths this release no longer ships, and only at content a + // release shipped: a file a member added or edited here is theirs. + const shippedNow = new Set(await walkFiles(srcDir)); + const retired = (PACKAGED_SKILL_FILES.get(skillName) ?? []).filter((p) => !shippedNow.has(p)); + const backupDir = skillBackupDir(baseDir, tool, path.relative(baseDir, path.dirname(destDir)), skillName); + const result = await removeOwnedFiles(destDir, await ownedSkillFiles(skillName, retired), baseDir, backupDir); + if (result.unbackedUp.length > 0) { + log.warn(`Kept ${result.unbackedUp.length} file(s) under ${destDir}: their backup could not be written, so they were not removed. First: ${result.unbackedUp[0].file} — ${result.unbackedUp[0].error}`); + } + if (result.notRemoved.length > 0) { + log.warn(`Archived but could not delete ${result.notRemoved.length} file(s) under ${destDir}. First: ${result.notRemoved[0].file} — ${result.notRemoved[0].error}`); + } + if (tool === CODEX_TOOL) await retireOtherCodexCopy(tool, skillName, destDir, target); deployed++; + deployedHere++; } catch (e) { log.error(`Failed to deploy built-in skill ${skillName} to ${toolPath.skills}: ${(e as Error).message}`); } } + + // The legacy trees go only once their replacement is in place: pruning first + // and then failing to write the stub (a link, a read-only directory) would + // leave the agent with no discoverable TeamAI skill at all. + if (deployedHere === skillNames.length) { + await pruneLegacyBuiltinSkills(tool, target); + } else { + log.warn(`Kept the pre-stub skills for ${tool}: the new stub was not deployed there, so removing them would leave nothing to discover.`); + } } return deployed; diff --git a/src/commands-reference.ts b/src/commands-reference.ts new file mode 100644 index 00000000..9d2510ab --- /dev/null +++ b/src/commands-reference.ts @@ -0,0 +1,84 @@ +import type { Command, Option } from 'commander'; + +// ─── Generated command reference ───────────────────────── +// +// The `core` skill used to carry a hand-written cheat sheet +// labelled "ground truth". It drifted four times (e151d43, +// 1ca43ac, 8bb0548, 2ddb546), each time after a command +// changed under it. +// +// The command table is the only real ground truth, so the +// reference is rendered from it and checked by a test that +// regenerates and diffs. Adding a command without updating +// the reference now fails the build. +// + +/** Where the rendered reference is written, relative to the package root. */ +export const COMMANDS_REFERENCE_PATH = 'skill-data/core/references/commands.md'; + +const HEADER = `# teamai command reference + +Every public command the installed CLI accepts, rendered from its own command +table. Hidden commands are left out: they are hook plumbing the CLI runs itself, +never something to type. Flags marked \`(hidden)\` work but are absent from +\`--help\`, so treat this file — not \`--help\` — as the complete list of flags. + +Generated: do not edit by hand. Regenerate with +\`npx vitest run commands-reference -u\` after changing a command or a flag. +`; + +function renderOption(option: Option): string { + const hidden = option.hidden ? ' (hidden)' : ''; + const description = option.description ? ` — ${option.description}` : ''; + return ` - \`${option.flags}\`${hidden}${description}`; +} + +function visibleOptions(command: Command): Option[] { + // `-h, --help` is on every command and says nothing about the command. + return command.options.filter((option) => option.long !== '--help'); +} + +/** + * Subcommands `--help` lists. Hidden ones (`track`, `contribute-check`, …) are + * hook plumbing the CLI calls itself; listing them would advertise them to the + * agent as supported commands. The implicit `help` entry says nothing. + */ +function visibleSubcommands(command: Command): Command[] { + return command.createHelp().visibleCommands(command).filter((sub) => sub.name() !== 'help'); +} + +function renderCommand(command: Command, parents: string[]): string[] { + const path = [...parents, command.name()]; + const args = command.registeredArguments.map((a) => { + const name = a.variadic ? `${a.name()}...` : a.name(); + return a.required ? `<${name}>` : `[${name}]`; + }); + const usage = ['teamai', ...path, ...args].join(' '); + + const lines: string[] = []; + const description = command.description(); + lines.push(`- \`${usage}\`${description ? ` — ${description}` : ''}`); + for (const option of visibleOptions(command)) { + lines.push(renderOption(option)); + } + for (const sub of visibleSubcommands(command)) { + lines.push(...renderCommand(sub, path).map((line) => ` ${line}`)); + } + return lines; +} + +/** Render the whole command table as the markdown the `core` skill serves. */ +export function renderCommandsReference(program: Command): string { + const sections: string[] = [HEADER]; + + const globalOptions = visibleOptions(program); + if (globalOptions.length > 0) { + sections.push(['## Global options', '', ...globalOptions.map(renderOption).map((l) => l.slice(2))].join('\n')); + } + + for (const command of visibleSubcommands(program)) { + sections.push([`## ${command.name()}`, '', ...renderCommand(command, [])].join('\n')); + } + + return sections.join('\n\n') + '\n'; +} diff --git a/src/config.ts b/src/config.ts index 9aeab136..ad03deef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -119,14 +119,22 @@ export async function saveState(state: State): Promise<void> { await writeJson(expandHome(getUserStatePath()), state); } +/** + * No teamai config on this machine for the scope asked: the file does not + * exist. Its own class so a command that can work without a team (the packaged + * skills) falls back on this and nothing else. A config file that exists but + * cannot be parsed, validated or migrated is a plain Error naming its path. + */ +export class NotInitializedError extends Error { + readonly name = 'NotInitializedError'; +} + /** * Require that teamai is initialized (local config exists) */ export async function requireInit(): Promise<{ localConfig: LocalConfig; teamConfig: TeamaiConfig }> { const localConfig = await loadLocalConfig(); - if (!localConfig) { - throw new Error('teamai is not initialized. Run `teamai init` first.'); - } + if (!localConfig) return throwMissingOrInvalid(expandHome(getUserConfigPath())); const teamConfig = await loadTeamConfig(localConfig.repo.localPath); if (!teamConfig) { throw new Error('Team config (teamai.yaml) not found. Check your repo path.'); @@ -134,6 +142,18 @@ export async function requireInit(): Promise<{ localConfig: LocalConfig; teamCon return { localConfig, teamConfig }; } +/** + * The loaders return null both when the config file is absent and when it could + * not be used (they log the reason). Only the first is "not initialized"; + * telling a member with a broken config to re-init sends them over a real setup. + */ +async function throwMissingOrInvalid(configPath: string, notInitializedMessage = 'teamai is not initialized. Run `teamai init` first.'): Promise<never> { + if (await pathExists(configPath)) { + throw new Error(`The teamai config at ${configPath} could not be read (the reason is logged above). Fix the file, or move it aside and run \`teamai init\` to write a new one.`); + } + throw new NotInitializedError(notInitializedMessage); +} + // ─── Scope-aware config loading ───────────────────────── /** @@ -272,7 +292,14 @@ export async function resolveDataHomeForScope(scope: Scope, projectRoot?: string return path.join(projectRoot, '.teamai'); } -export async function detectProjectConfig(cwd?: string): Promise<LocalConfig | null> { +/** + * Told about a project-scope config file that exists but cannot be used, which + * detection otherwise skips: `null` means "no project config here" to every + * caller that does not ask. + */ +export type UnreadableConfigSink = (configPath: string, error: string) => void; + +export async function detectProjectConfig(cwd?: string, onUnreadable?: UnreadableConfigSink): Promise<LocalConfig | null> { const dir = cwd ?? process.cwd(); // Resolve git anchors FIRST so the result never depends on which directory of @@ -293,23 +320,23 @@ export async function detectProjectConfig(cwd?: string): Promise<LocalConfig | n // `<basename>-<hash>` name; adoption renames it into the current name so // detection — and every command after it — keeps finding the config. const partitionDir = await resolvePartitionDir(anchors.projectAnchor); - const fromPartition = await readConfigFrom(partitionDir, anchors.workspaceRoot); + const fromPartition = await readConfigFrom(partitionDir, anchors.workspaceRoot, undefined, onUnreadable); if (fromPartition) return fromPartition; // 2. No partition config yet. A workspace that declares `mode: self` self-heals // on a fresh clone (issue #198): bootstrapSelfRepo now writes the machine // config into the PARTITION (P2), not <workspaceRoot>/.teamai. So run the // self-heal and, on success, read the config back FROM THE PARTITION. - const healed = await selfHealAndReadPartition(anchors.workspaceRoot, partitionDir); + const healed = await selfHealAndReadPartition(anchors.workspaceRoot, partitionDir, onUnreadable); if (healed) return healed; // 3. Otherwise read a legacy `<workspaceRoot>/.teamai` config directly — a // pre-P2 self install (or any un-migrated install) whose config still lives // in the repo. Double-read compat until migration relocates it. - return readConfigFrom(legacyDir, anchors.workspaceRoot); + return readConfigFrom(legacyDir, anchors.workspaceRoot, undefined, onUnreadable); } // Not a git repo: fall back to a legacy `.teamai` directly at `dir` (also runs // the self-heal bootstrap for a freshly-cloned single-repo project). - return readConfigFrom(path.join(dir, '.teamai'), dir, dir); + return readConfigFrom(path.join(dir, '.teamai'), dir, dir, onUnreadable); } /** @@ -337,6 +364,7 @@ export async function detectProjectConfig(cwd?: string): Promise<LocalConfig | n async function selfHealAndReadPartition( workspaceRoot: string, partitionDir: string, + onUnreadable?: UnreadableConfigSink, ): Promise<LocalConfig | null> { try { const { bootstrapSelfRepo } = await import('./bootstrap.js'); @@ -345,13 +373,14 @@ async function selfHealAndReadPartition( } catch { return null; } - return readConfigFrom(partitionDir, workspaceRoot); + return readConfigFrom(partitionDir, workspaceRoot, undefined, onUnreadable); } export async function readConfigFrom( dataHomeDir: string, projectRoot: string, selfHealRepoRoot?: string, + onUnreadable?: UnreadableConfigSink, ): Promise<LocalConfig | null> { const configPath = path.join(dataHomeDir, 'config.yaml'); if (!(await pathExists(configPath))) { @@ -366,7 +395,11 @@ export async function readConfigFrom( if (!(await pathExists(configPath))) return null; } const content = await readFileSafe(configPath); - if (!content) return null; + if (!content) { + // The file exists (checked above) but gave nothing: unreadable or empty. + onUnreadable?.(configPath, 'the file is empty or could not be read'); + return null; + } try { const raw = YAML.parse(content); const config = LocalConfigSchema.parse(raw); @@ -394,11 +427,26 @@ export async function readConfigFrom( }; } return resolved; - } catch { + } catch (e) { + onUnreadable?.(configPath, (e as Error).message); return null; } } +/** + * The project-scope config under `cwd` that exists but cannot be read, parsed + * or validated, with the reason, or null. Detection skips such a file and falls + * back to the next candidate — a legacy `.teamai/`, then the user config — + * which for a command that must know which team it serves means answering for + * the wrong one. So a broken higher-priority file is reported even when a later + * candidate loads. + */ +export async function findUnreadableProjectConfig(cwd?: string): Promise<string | null> { + let problem: string | null = null; + await detectProjectConfig(cwd, (configPath, error) => { problem ??= `${configPath}: ${error}`; }); + return problem; +} + /** * Require init for a specific scope. * For 'user' scope, behaves like original requireInit. @@ -410,11 +458,10 @@ export async function requireInitForScope( ): Promise<{ localConfig: LocalConfig; teamConfig: TeamaiConfig }> { const localConfig = await loadLocalConfigForScope(scope, projectRoot); if (!localConfig) { - throw new Error( - scope === 'project' - ? `teamai is not initialized in project scope at ${projectRoot}. Run \`teamai init\` first.` - : 'teamai is not initialized. Run `teamai init` first.', - ); + if (scope === 'project') { + throw new NotInitializedError(`teamai is not initialized in project scope at ${projectRoot}. Run \`teamai init\` first.`); + } + return throwMissingOrInvalid(expandHome(getConfigPath(scope, projectRoot))); } const teamConfig = await loadTeamConfig(localConfig.repo.localPath); if (!teamConfig) { diff --git a/src/contribute-check.ts b/src/contribute-check.ts index b1d2ac06..1af35b69 100644 --- a/src/contribute-check.ts +++ b/src/contribute-check.ts @@ -493,8 +493,8 @@ function buildHint({ friction, promptSummary, isKnowledgeGap }: HintContext): st } const task = promptSummary ? `\n\nTask: ${promptSummary}` : ''; const action = isKnowledgeGap - ? 'Consider running /teamai-share-learnings to summarize what you learned, share it with your team, and fill the knowledge gap.' - : 'Consider running /teamai-share-learnings to summarize what you learned and share it with your team.'; + ? 'Consider running `/teamai share what this session taught me` to summarize what you learned, share it with your team, and fill the knowledge gap (or run `teamai skill get share`).' + : 'Consider running `/teamai share what this session taught me` to summarize what you learned and share it with your team (or run `teamai skill get share`).'; return `${headline}${task}\n\n${action}`; } diff --git a/src/hook-handlers.ts b/src/hook-handlers.ts index df1e45bb..f28ee95c 100644 --- a/src/hook-handlers.ts +++ b/src/hook-handlers.ts @@ -233,17 +233,27 @@ const trackSlashHandler: HookHandler = { /** * Whether the share-learnings hint may be emitted at all. Resolved lazily per * hook run so a team can switch it off via teamai.yaml (or a member via local - * config) without re-injecting hooks. Falls back to enabled when config can't - * be read, preserving pre-toggle behavior for half-initialized installs. + * config) without re-injecting hooks. Falls back to enabled when there is no + * config at all, preserving pre-toggle behavior for half-initialized installs, + * where `teamai skill get share` serves too. A config that exists but cannot be + * loaded withholds it: `share` refuses there, so the nudge would lead nowhere. + * + * Recall and a writable source gate it too: the hint routes to the `share` + * workflow, and `teamai skill get share` refuses while recall is off or the + * team source is read-only HTTP, so a nudge towards it would send the agent to + * a command that says no. The dispatcher already drops this `gitOnly` handler + * for HTTP teams; the check here keeps the gate the same wherever it is called. */ async function contributeHintAllowed(): Promise<boolean> { - const { isContributeHintEnabled } = await import('./types.js'); + const { isContributeHintEnabled, isRecallEnabled } = await import('./types.js'); + const { autoDetectInit, NotInitializedError } = await import('./config.js'); try { - const { autoDetectInit } = await import('./config.js'); const { localConfig, teamConfig } = await autoDetectInit(); - return isContributeHintEnabled(localConfig, teamConfig); - } catch { - return isContributeHintEnabled({}, {}); + return localConfig.repo?.kind !== 'http' + && isContributeHintEnabled(localConfig, teamConfig) + && isRecallEnabled(localConfig, teamConfig); + } catch (e) { + return e instanceof NotInitializedError ? isContributeHintEnabled({}, {}) : false; } } diff --git a/src/index.ts b/src/index.ts index 87ef267d..d6bc5076 100644 --- a/src/index.ts +++ b/src/index.ts @@ -153,20 +153,43 @@ program const skillCmd = program .command('skill') - .description('List and inspect skills (default: list all skills across repo + installed agents)') + .description('List and inspect skills (default: repo + installed agents, then the CLI-served catalog)') .action(async () => { const globalOpts = program.opts() as GlobalOptions; - const { list } = await import('./status.js'); - await list('skills', { ...globalOpts, source: 'all' }); + const { skillList } = await import('./skill-cmd.js'); + await skillList(globalOpts); }); skillCmd .command('list') - .description('List all skills (alias for: teamai list skills --source all)') - .action(async () => { + .description('List team and installed skills, then the built-in catalog the CLI serves') + .option('--json', 'Output the CLI-served built-in skill catalog as JSON') + .action(async (cmdOpts) => { const globalOpts = program.opts() as GlobalOptions; - const { list } = await import('./status.js'); - await list('skills', { ...globalOpts, source: 'all' }); + const { skillList } = await import('./skill-cmd.js'); + await skillList({ ...globalOpts, ...cmdOpts }); + }); + +skillCmd + // Optional so that `--all` needs no name; the action fails when both are missing. + .command('get [names...]') + .description('Print built-in skill content served by the installed CLI') + .option('--full', 'Append the skill\'s references/ and templates/ files') + .option('--all', 'Print every skill the CLI serves') + // A hallucinated flag should cost a warning, not a failed command: unknown + // options fall through to the action, which reports and ignores them. + .allowUnknownOption() + .action(async (names: string[] | undefined, cmdOpts) => { + const { skillGet } = await import('./skill-content.js'); + await skillGet(names ?? [], { full: cmdOpts.full, all: cmdOpts.all }); + }); + +skillCmd + .command('path <name>') + .description('Print the packaged directory of a built-in skill (for scripts and templates)') + .action(async (name: string) => { + const { skillPath } = await import('./skill-content.js'); + await skillPath(name); }); skillCmd @@ -784,7 +807,7 @@ program }); program - .command('hook-dispatch <event>') + .command('hook-dispatch <event>', { hidden: true }) .description('Unified hook dispatcher — handles all teamai hooks for a given event in one process') .option('--stdin', 'Read hook data from STDIN (accepted for forward compat, always reads STDIN)') .option('--tool <name>', 'Tool identifier (e.g. codebuddy, workbuddy, claude)') @@ -1227,4 +1250,14 @@ async function publishMaintenance(localConfig: LocalConfig, message: string): Pr } } -program.parse(); +/** + * The command table doubles as the source of truth for the generated skill + * command reference (skill-data/core/references/commands.md). Importing this + * module with TEAMAI_COMMAND_TABLE_ONLY set yields `program` without running + * the CLI. Test-only: the two tests that read the table set it. + */ +export { program }; + +if (!process.env.TEAMAI_COMMAND_TABLE_ONLY) { + program.parse(); +} diff --git a/src/init.ts b/src/init.ts index 4ebfce97..e2e0efe1 100644 --- a/src/init.ts +++ b/src/init.ts @@ -17,7 +17,6 @@ import { type Scope, getTeamaiHome, getConfigPath, - isRecallEnabled, } from './types.js'; import { getUserHome } from './utils/home.js'; import { describeRoles, listRoleIds, loadRolesManifest } from './roles.js'; @@ -1614,26 +1613,32 @@ export async function init(options: GlobalOptions & { // Step 7: Inject built-in + team hooks into AI tools const reloadedTeamConfig = await loadTeamConfig(localPath); + // Only a stub that actually landed is announced as ready in the IDE. + let stubDeployed = 0; if (reloadedTeamConfig) { const filterAgents = requestedAgents.length > 0 ? requestedAgents : undefined; await reconcileTeamHooksForConfig(reloadedTeamConfig, localConfig, { filterAgents }); - // Step 7.5: Deploy CLI built-in skills immediately so team-wiki-codebase - // is available in the IDE right after init, without waiting for first pull. + // Step 7.5: Deploy the built-in discovery stub immediately so the teamai + // skill is available in the IDE right after init, without waiting for the + // first pull. Its workflows are served by `teamai skill get`. try { const { deployBuiltinSkills } = await import('./builtin-skills.js'); - const skipRecall = !isRecallEnabled(localConfig, reloadedTeamConfig); - const deployed = await deployBuiltinSkills(reloadedTeamConfig, localConfig, { skipRecall }); - if (deployed > 0) { - log.debug(`Deployed ${deployed} built-in skill(s)`); + stubDeployed = await deployBuiltinSkills(reloadedTeamConfig, localConfig); + if (stubDeployed > 0) { + log.debug(`Deployed ${stubDeployed} built-in skill(s)`); } } catch (e) { - log.debug(`Built-in skills deployment skipped: ${(e as Error).message}`); + log.warn(`The built-in teamai skill was not deployed: ${(e as Error).message}`); } } log.success('teamai initialized successfully!'); - log.info('Built-in skills (e.g. team-wiki-codebase) are ready to use in your IDE now.'); + if (stubDeployed > 0) { + log.info('The built-in teamai skill is ready in your IDE; it loads its workflows with `teamai skill get`.'); + } else { + log.warn('The built-in teamai skill was not deployed to any AI tool (see the lines above); run `teamai pull` once the cause is fixed, or `teamai doctor` to see it.'); + } log.info('Skills, rules, env and docs auto-sync on each session start when the selected agent has active TeamAI hooks.'); log.info('Run `teamai status` to check current config.'); diff --git a/src/packaged-skill-digests.ts b/src/packaged-skill-digests.ts new file mode 100644 index 00000000..42598815 --- /dev/null +++ b/src/packaged-skill-digests.ts @@ -0,0 +1,40 @@ +/** + * sha256 of every file a release shipped under the CLI-owned skill trees, by + * skill and path relative to the skill directory, whole file. The deploy + * repaired frontmatter from 0.16.1 on, but every SKILL.md those releases + * shipped was already complete, so what is on disk is what was shipped. + * + * Generated from `git ls-tree -r <ref> -- skills/` over all 100 tags + * through v0.25.0 plus origin/main before the stub (installs from `main`), + * minus `teamai-wiki` (see PACKAGED_SKILL_FILES). Do not edit by hand; a new + * release adds nothing here, since the package no longer ships these trees. + */ +export const PACKAGED_SKILL_DIGESTS: ReadonlyMap<string, ReadonlyMap<string, readonly string[]>> = new Map([ + ['teamai', new Map([ + ['SKILL.md', ['82a22b1c37531834ab5784f93f950f0a8840ac4e4aa422f1ef2dc03437f9ad66', '898fc8a74fec0ccba37b0a6c7a8a446d3dde97a486fc566340f80c259faf4211', 'a523cfc79aca5870f6f35ff639229777f4f506457666560ec21f57176cf7f78e']], + ['references/contribute-member.md', ['2a6a8c3eeee7424f79cb6d3f97d14f8588720f1d570028f5afdd12d6db458355']], + ['references/join-member.md', ['2f2f823675cea971b2a0360e7c6f090b2397e25702e60cce50bafb2b450ce8c3', '45e56c965fba5fe5f14cfb927c9b18032d19ebf98e524e7c709bcde16862b15a', '9584d1f228930c01141dc47a19623c17b02df999b0bff19e104d1a18fc952fcb', 'ed2ab1c92680b3412e2ce60d5900f6baba0da896ad92158945ac6115ac939fc2']], + ['references/manage-admin.md', ['fd31d78724fb35d3bfecf606299c9e907f2a10da275bf06f840780c674584158']], + ['references/provider-tgit.md', ['386a14715db132b5b34ab95a7586119b79a6795cbbbe776a4be455b1af7fa49f', 'df7faedb8beeafeb55a23c2b3d2b99d1421548cf4f8172ed7da0752ef6aeda39']], + ['references/setup-admin.md', ['4e58bae91bcb3831fd7d2dc0c9f086bb0e985f7d51dd7bc7e753bce1b380816e', '615fc60bab798125bc2b6340e87b3a1e5376b7d499cab5d884f9d78bab07154b', 'dae12585f3003f1e6c347f51859de68c7af9baf11b84be63559cb782d122db7b', 'ff9996686f42cc7d7c2a09cb5f254dc0d8fb379fae29dab9046c971dcced543f']], + ['references/troubleshooting.md', ['78ad122c14f1c5081ef698c880c4a250a99eb7a6ccbb1b39674359e712261fc6']], + ['references/uninstall.md', ['10a97318e8bc6a94a0413e6d1b1b516b24ab8fd7f52d118cfc0d92c97394b892']], + ])], + ['teamai-share-learnings', new Map([ + ['SKILL.md', ['2bfdfa9c4f312424e06544fe104fbf5988cf5a6b3f2289215cbe89fb430d18c0', '676da346cf38c3d40d681a28bd2330abf825ec534ed774996362b3a91d20981a', '7771ec3997b747e4e270818189a1f450cc7e7307cc14c15b42491a2f20e94494', 'a47169735ee710bb38c21fa72e8f647ca2078b1cfd538bad78e30a0806f179c5', 'e7ab73f2258e13b55c91bffdd07975b13fb7a34c84c81215340a8239432f358b', 'f2d7c437520d8707182fbd3b4ca0dd453315d5210d924abb7dfa25aee651a94f']], + ])], + ['team-wiki-codebase', new Map([ + ['README.md', ['4e1ab336bfa6d78085572b4e0fc0e345bda0ec2be065279f189a8f2939f242b8', '82945615d4706b2c1b581d2b326c15536be0b8573472c359d1414690f0b2e804', 'd3c7312663caa8cefccd1034127fe7091384b8e603fcd04961f4f30a8ff1fe0e']], + ['SKILL.md', ['47ebc8f3ac3f39551e96ef46ede14e3e076fe68acee3b431c5598b09df433904', '4d7e728e0c821404d760a63f994e6d0fe81519bd875e2d4297d178bffd292266', '4e6f1b937270e90cf53351e117cf8a4de19cbcca37b90603abe9532a9fe3a4c0', 'f6ebd80e036cc37dd049597c20bea5c28f267729d8c6af636ee5c61f60092af5', 'fad8ee99235ca195438dd4e52853d42c8f53b09bed4febd0f330039dee804907']], + ['references/agents/graph-rag-agent.md', ['d79e52cfec1e131877f7fa28bb14993b937fb643ed6778fde6bf569dbd0ba2d3']], + ['references/agents/kb-doc-generator.md', ['8dc1f1ef5e5d270223586567629b66333a07c42ae103ac5cdef6c755364587d5']], + ['references/methodology/phase0-collection.md', ['1061ff28e17aa290dc0942958bac0ea0844b3b830fabda3dd9e03ac32c02b0b0', '89caa5e6e5135b19e39ebf48224a31ae6ea80bbef84c7c2e4cf50b6391220cf6']], + ['references/methodology/phase1-reverse-engineering.md', ['9d709e09a30ca198020fe7f2470980a4d2f4889a685dc33f71c79f6433110a59']], + ['references/methodology/phase2-document-types.md', ['62eee3e3290cbc1a4a7c40bcac5e7d8f2100989b438726b867af448dfda67b7b']], + ['references/methodology/phase3-ai-enhancement.md', ['efe1536f1ea4a2ffeb9ab69409ce1cb172fe8a5b8efb583a46ed69ed976bc6d0']], + ['references/methodology/phase4-quality.md', ['a7b536ab120a8c4bc3fbd53256675309a399e53b4d0d202abd5df1b82c985754']], + ['references/templates/project-overview.md', ['296c15c827ae7798bf9f3112b84d1bdd69b0807bb80286cfc78f1a0d956e66ec']], + ['scripts/scan_repo.py', ['a941f3ac9a260c860cfeded26eb6c6f3d55cc5af748e1e5f673a94278c6a9c36']], + ['scripts/validate_kb.py', ['c6e08b03b80a60048374637b4de20afdd21b552892e915b8301efb368316522f']], + ])], +]); diff --git a/src/pull.ts b/src/pull.ts index f0627bad..eabdd31e 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -70,8 +70,7 @@ export interface RolePullContext { * * - git: `git pull` into localPath; version = current HEAD rev. * - http: nothing to clone — skills/rules/CLAUDE.md are delivered per-session via - * report/sync/ack (the local-agent bypass), not a repo snapshot. The - * `reportingOnly` flag tells the deploy step to skip git-tree sync. + * report/sync/ack (the local-agent bypass), not a repo snapshot. * * Returns a display label and the opaque version string used as the * incremental-sync cache key (state.lastPullRev). `version` is null only when @@ -85,7 +84,7 @@ export interface RolePullContext { */ async function refreshTeamRepo( localConfig: LocalConfig, -): Promise<{ label: string; version: string | null; reportingOnly: boolean; submodulesFailed: boolean; submodulesChanged: boolean }> { +): Promise<{ label: string; version: string | null; submodulesFailed: boolean; submodulesChanged: boolean }> { if (localConfig.repo.kind === 'http') { const { resolveApiKey } = await import('./api-key.js'); const apiKey = resolveApiKey(); @@ -94,7 +93,7 @@ async function refreshTeamRepo( } // HTTP backends deliver resources through report/sync (own hook handler), // so there is no repo tree to pull here. - return { label: 'HTTP (report/sync delivery)', version: null, reportingOnly: true, submodulesFailed: false, submodulesChanged: false }; + return { label: 'HTTP (report/sync delivery)', version: null, submodulesFailed: false, submodulesChanged: false }; } if (localConfig.repo.kind === 'self') { @@ -118,7 +117,7 @@ async function refreshTeamRepo( } catch { version = null; } - return { label: 'single-repo (knowledge on main)', version, reportingOnly: false, submodulesFailed: false, submodulesChanged: false }; + return { label: 'single-repo (knowledge on main)', version, submodulesFailed: false, submodulesChanged: false }; } // The shared team clone is mutated here (git pull + flushPendingLearnings' @@ -193,7 +192,7 @@ async function refreshTeamRepo( log.warn(`Submodule update failed for ${localConfig.repo.localPath}: ${(e as Error).message}`); } - return { label: result, version, reportingOnly: false, submodulesFailed, submodulesChanged }; + return { label: result, version, submodulesFailed, submodulesChanged }; } /** teamai.yaml `usageReport: false` — per-repo opt-out of stat commits. */ @@ -757,10 +756,6 @@ async function pullForScope( // Step 1: refresh team repo (git pull, or HTTP /repo materialization) const pullSpin = spinner(`[${scopeLabel}] Pulling team repo...`).start(); let currentRev: string | null = null; - // Reporting-only HTTP endpoints have no team repo to write to, so the - // team-repo-dependent built-in skill (teamai-share-learnings) is useless - // there and must not be injected. - let reportingOnly = false; // A failed submodule update holds the rev back below so the next pull // retries (see refreshTeamRepo). let submodulesFailed = false; @@ -770,7 +765,6 @@ async function pullForScope( try { const refresh = await refreshTeamRepo(localConfig); currentRev = refresh.version; - reportingOnly = refresh.reportingOnly; submodulesFailed = refresh.submodulesFailed; submodulesChanged = refresh.submodulesChanged; pullSpin.succeed(`[${scopeLabel}] Team repo: ${refresh.label}`); @@ -1021,7 +1015,7 @@ async function pullForScope( const skipRecall = !isRecallEnabled(localConfig, freshConfig); try { const { deployBuiltinAgents } = await import('./builtin-agents.js'); await deployBuiltinAgents(freshConfig, localConfig, { skipRecall }); } catch {} try { const { deployBuiltinRules } = await import('./builtin-rules.js'); await deployBuiltinRules(freshConfig, localConfig, { skipRecall }); } catch {} - try { const { deployBuiltinSkills } = await import('./builtin-skills.js'); await deployBuiltinSkills(freshConfig, localConfig, { reportingOnly, skipRecall }); } catch {} + try { const { deployBuiltinSkills } = await import('./builtin-skills.js'); await deployBuiltinSkills(freshConfig, localConfig); } catch {} // Refresh managed culture/shared-instruction blocks as well. A CLI // upgrade may add a new target file while the team repo SHA and tool // target set remain unchanged. @@ -1295,8 +1289,7 @@ async function pullForScope( if (!options.dryRun) { try { const { deployBuiltinSkills } = await import('./builtin-skills.js'); - const skipRecallForSkills = !isRecallEnabled(localConfig, freshConfig); - const deployed = await deployBuiltinSkills(freshConfig, localConfig, { reportingOnly, skipRecall: skipRecallForSkills }); + const deployed = await deployBuiltinSkills(freshConfig, localConfig); if (deployed > 0) { log.debug(`[${scopeLabel}] Deployed ${deployed} built-in skill(s)`); } diff --git a/src/recall-toggle.ts b/src/recall-toggle.ts index d2fbd51a..a2142ff2 100644 --- a/src/recall-toggle.ts +++ b/src/recall-toggle.ts @@ -9,7 +9,7 @@ import { type ToolName, } from './resources/agent-format.js'; import { ruleFileExtensionForTool } from './resources/rule-format.js'; -import { RECALL_DEPENDENT_SKILLS } from './builtin-skills.js'; +import { LEGACY_RECALL_SKILL_NAMES, builtinSkillsTarget, pruneLegacyBuiltinSkills } from './builtin-skills.js'; import { resolveToolBaseDir, isRecallEnabled, @@ -38,6 +38,17 @@ async function removeRecallArtifacts(teamConfig: TeamaiConfig, localConfig: Loca } } + // Remove the legacy recall skill an earlier release deployed. The served + // `share` workflow is gated at run time, but a member who upgrades and + // disables recall before pulling still has the old directory. + // Same resolver and gates as deployment: an uninstalled Codex must not have + // the shared .agents/skills root pruned on its behalf, and OpenClaw and + // Hermes are pruned where their skills actually live. + if (toolPath.skills && !isAgentExcluded(localConfig, tool)) { + const target = await builtinSkillsTarget(tool, toolPath.skills, localConfig); + if (target) await pruneLegacyBuiltinSkills(tool, target, LEGACY_RECALL_SKILL_NAMES); + } + // Remove recall agent file if (toolPath.agents) { const agentsDir = path.join(baseDir, toolPath.agents); @@ -54,17 +65,6 @@ async function removeRecallArtifacts(teamConfig: TeamaiConfig, localConfig: Loca } } - // Remove recall-dependent built-in skills - if (toolPath.skills) { - for (const skillName of RECALL_DEPENDENT_SKILLS) { - const skillDir = path.join(baseDir, toolPath.skills, skillName); - if (await pathExists(skillDir)) { - await remove(skillDir); - log.debug(`Removed recall skill ${skillName} from ${tool}`); - } - } - } - // Remove recall block from CLAUDE.md if (toolPath.claudemd) { const claudeMdPath = path.join(baseDir, toolPath.claudemd); @@ -95,7 +95,7 @@ async function deployRecallArtifacts(teamConfig: TeamaiConfig, localConfig: Loca await deployBuiltinRules(teamConfig, localConfig, { skipRecall: false }); await deployBuiltinAgents(teamConfig, localConfig, { skipRecall: false }); - await deployBuiltinSkills(teamConfig, localConfig, { skipRecall: false }); + await deployBuiltinSkills(teamConfig, localConfig); // Inject recall rules block into CLAUDE.md for Tier-1 tools const { injectClaudeMdSection } = await import('./utils/claudemd.js'); diff --git a/src/resources/skills.ts b/src/resources/skills.ts index cdde62f6..8fcf8593 100644 --- a/src/resources/skills.ts +++ b/src/resources/skills.ts @@ -5,7 +5,7 @@ import type { ResourceItem, ResourceItemStatus, DeliveryTarget, TeamaiConfig, Lo import { getPushignorePath, isAgentExcluded, resolveToolBaseDir, scopedToolPaths } from '../types.js'; import { listDirs, listFilesRecursive, pathExists, copyDir, remove, pruneEmptyDirs, dirContentEqual, dirTeamSubsetEqual, getDirLatestMtime, readFileSafe, writeFile } from '../utils/fs.js'; import { log } from '../utils/logger.js'; -import { BUILTIN_SKILL_NAMES } from '../builtin-skills.js'; +import { isCliOwnedSkillName } from '../builtin-skills.js'; import { resolveOpenclawWorkspaceDir } from '../openclaw-hooks.js'; import { getHermesHome } from '../hermes-home.js'; import { loadRolesManifest, resolveRoleResourceNamespaces } from '../roles.js'; @@ -15,8 +15,8 @@ import { splitFrontmatter, stringifyFrontmatter } from '../utils/frontmatter.js' /** File name used to track who has contributed (pushed) a skill. */ const CONTRIBUTORS_FILE = 'CONTRIBUTORS'; const SKILL_MD = 'SKILL.md'; -const CODEX_TOOL = 'codex'; -const SHARED_AGENT_SKILLS_PATH = '.agents/skills'; +export const CODEX_TOOL = 'codex'; +export const SHARED_AGENT_SKILLS_PATH = '.agents/skills'; /** Prefer Codex's shared skill when that skill already lives there. */ export async function resolveSkillDestination( @@ -440,7 +440,7 @@ export class SkillsHandler extends ResourceHandler { if (tombstones.has(dir)) continue; if (pushIgnoredSkills.has(dir)) continue; if (blockedSkills.has(dir)) continue; // Skip skills in non-allowed namespaces - if (BUILTIN_SKILL_NAMES.has(dir)) continue; // Skip CLI built-in skills + if (isCliOwnedSkillName(dir)) continue; // Skip CLI built-in skills, current and legacy if (sourceSkillNames.has(dir)) continue; // Skip cross-team source skills if (teamSkills.has(dir)) { diff --git a/src/skill-cmd.ts b/src/skill-cmd.ts index 485aa2f5..fe6786dd 100644 --- a/src/skill-cmd.ts +++ b/src/skill-cmd.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { autoDetectInit } from './config.js'; +import { autoDetectInit, NotInitializedError } from './config.js'; import { log } from './utils/logger.js'; import { listDirs, pathExists } from './utils/fs.js'; import { SkillsHandler } from './resources/skills.js'; @@ -13,20 +13,32 @@ import { type SkillSource, } from './agent-skills.js'; import { detectInstalledAgents, type ResolvedAgent } from './known-agents.js'; -import type { GlobalOptions, LocalConfig } from './types.js'; +import { LEGACY_BUILTIN_SKILL_NAMES } from './builtin-skills.js'; +import { blockMessage, resolveServableSkill, skillCatalog, type SkillBlockReason } from './skill-content.js'; +import type { GlobalOptions, LocalConfig, TeamaiConfig } from './types.js'; const DESCRIPTION_MAX = 160; interface ResolvedSkill { + kind: 'found'; name: string; /** Path used to read SKILL.md, contributors and description. */ primaryPath: string; /** Where the primary copy was discovered. */ - primaryOrigin: 'team' | 'agent'; + primaryOrigin: 'team' | 'agent' | 'builtin'; /** Optional namespace if found in the team repo. */ namespace?: string; } +/** A packaged skill the serving gate withholds; there is no path to print. */ +interface BlockedSkill { + kind: 'blocked'; + name: string; + reason: SkillBlockReason; +} + +type LocatedSkill = ResolvedSkill | BlockedSkill; + /** * `teamai skill show <name>` — print metadata about a single * skill: source classification, contributors, namespace, tags @@ -37,30 +49,80 @@ interface ResolvedSkill { * we print under "Repo path" or "Installed in". */ export async function skillShow(name: string, options: GlobalOptions): Promise<void> { - const { localConfig, teamConfig } = await autoDetectInit(); + let init: { localConfig: LocalConfig; teamConfig: TeamaiConfig }; + try { + init = await autoDetectInit(); + } catch (e) { + // A packaged skill needs no team: it ships with the CLI, so `teamai skill + // show core` still works on a machine that has never run `teamai init`. + // Only that case: a broken config is reported, not read as "no team". + if (!(e instanceof NotInitializedError)) throw e; + const packaged = await resolveServableSkill(name); + if (packaged.kind === 'blocked') { + const { headline, hint } = blockMessage(packaged.name, packaged.reason); + log.error(headline); + log.dim(hint); + process.exitCode = 1; + return; + } + if (packaged.kind !== 'found') { + log.error(`Skill "${name}" not found among the skills the installed CLI serves.`); + log.dim('Run `teamai init` first to search the team repo and installed agents too.'); + process.exitCode = 1; + return; + } + printSkillCard({ + name: packaged.skill.name, + source: { kind: 'builtin' }, + description: truncate(await readSkillDescription(path.join(packaged.skill.dir, 'SKILL.md')), DESCRIPTION_MAX), + contributors: [], + tags: [], + primaryPath: packaged.skill.dir, + primaryOrigin: 'builtin', + installedIn: [], + }); + log.dim('No team is set up on this machine, so contributors, tags and installed agents are not shown.'); + return; + } + const { localConfig, teamConfig } = init; const agents = await detectInstalledAgents(localConfig, teamConfig); - const resolved = await locateSkill(name, localConfig, agents); - if (!resolved) { + const located = await locateSkill(name, localConfig, agents); + if (!located) { log.error(`Skill "${name}" not found in team repo or any installed agent.`); log.dim('Try `teamai list --source all` to see available skills.'); process.exitCode = 1; return; } + // The resolver never hands out a blocked skill, so there is no directory to + // print here even by accident; only the refusal is left to do. + if (located.kind === 'blocked') { + const { headline, hint } = blockMessage(located.name, located.reason); + log.error(headline); + log.dim(hint); + process.exitCode = 1; + return; + } + const resolved: ResolvedSkill = located; + + const resolvedName = resolved.name; - const ctx = await buildClassifyContext(localConfig); - const source = classifySkill(name, ctx); + // A skill served from the package is built in by construction; BUILTIN_SKILL_NAMES + // only knows the deployed stub, so classifying by name would call `core` local-only. + const source: SkillSource = resolved.primaryOrigin === 'builtin' + ? { kind: 'builtin' } + : classifySkill(resolvedName, await buildClassifyContext(localConfig)); const description = truncate(await readSkillDescription(path.join(resolved.primaryPath, 'SKILL.md')), DESCRIPTION_MAX); const contributors = await SkillsHandler.readContributors(resolved.primaryPath); const tagsConfig = await loadTagsConfig(localConfig.repo.localPath); - const tags = tagsConfig?.skills?.[name] ?? []; + const tags = tagsConfig?.skills?.[resolvedName] ?? []; - const installedIn = await collectInstalledAgents(name, agents); + const installedIn = await collectInstalledAgents(resolvedName, agents); printSkillCard({ - name, + name: resolvedName, source, namespace: resolved.namespace ?? (source.kind === 'team' ? source.namespace : undefined), description, @@ -77,17 +139,63 @@ export async function skillShow(name: string, options: GlobalOptions): Promise<v } } +/** + * `teamai skill` / `teamai skill list` — the repo and installed-agent listing, + * plus the catalog the installed CLI serves on demand. + */ +export async function skillList(options: GlobalOptions & { json?: boolean }): Promise<void> { + const catalog = await skillCatalog(); + + if (options.json) { + console.log(JSON.stringify({ skills: catalog }, null, 2)); + return; + } + + // The packaged catalog needs no team: a machine that has not run `teamai init` + // still gets to discover what the installed CLI serves, like `skill get` does. + let initialized = true; + try { + await autoDetectInit(); + } catch (e) { + if (!(e instanceof NotInitializedError)) throw e; + initialized = false; + } + if (initialized) { + const { list } = await import('./status.js'); + await list('skills', { ...options, source: 'all' }); + } else { + log.dim('Not initialized: run `teamai init` to list team and installed skills.'); + console.log(''); + } + + console.log('=== BUILT-IN SKILLS (served by the CLI) ==='); + console.log(''); + if (catalog.length === 0) { + console.log(' (none — the installed package ships no skill content)'); + } else { + for (const entry of catalog) { + const note = entry.blockedBy === 'recall' ? ' (needs recall — teamai recall enable)' + : entry.blockedBy === 'read-only' ? ' (not available on a read-only HTTP source)' + : entry.blockedBy === 'config' ? ' (not available: the teamai config could not be loaded)' : ''; + console.log(` ${entry.name}${note}`); + console.log(` ${truncate(entry.description, DESCRIPTION_MAX) || '(no description)'}`); + console.log(` teamai skill get ${entry.name}`); + } + } + console.log(''); +} + async function locateSkill( name: string, localConfig: LocalConfig, agents: ResolvedAgent[], -): Promise<ResolvedSkill | null> { +): Promise<LocatedSkill | null> { const teamSkillsDir = path.join(localConfig.repo.localPath, 'skills'); // 1. Flat layout in team repo const flat = path.join(teamSkillsDir, name); if (await pathExists(path.join(flat, 'SKILL.md'))) { - return { name, primaryPath: flat, primaryOrigin: 'team' }; + return { kind: 'found', name, primaryPath: flat, primaryOrigin: 'team' }; } // 2. Namespaced layout in team repo @@ -96,20 +204,35 @@ async function locateSkill( for (const ns of namespaces) { const candidate = path.join(teamSkillsDir, ns, name); if (await pathExists(path.join(candidate, 'SKILL.md'))) { - return { name, primaryPath: candidate, primaryOrigin: 'team', namespace: ns }; + return { kind: 'found', name, primaryPath: candidate, primaryOrigin: 'team', namespace: ns }; } } } - // 3. First installed agent that has the skill - for (const agent of agents) { + // 3. First installed agent that has the skill. Ahead of the packaged content + // on purpose: `codebase`, `default`, `learning` and `share` are ordinary + // names, and a directory a member created under one of them is the skill + // they are asking about, not the built-in it happens to alias. A legacy + // built-in name is the exception: that directory is a stale copy a + // pre-stub release wrote, so the name goes to the packaged skill and its + // gate, never to the leftover a pull has not pruned yet. + for (const agent of LEGACY_BUILTIN_SKILL_NAMES.has(name) ? [] : agents) { if (!agent.installed) continue; const candidate = path.join(agent.absoluteSkillsPath, name); if (await pathExists(path.join(candidate, 'SKILL.md'))) { - return { name, primaryPath: candidate, primaryOrigin: 'agent' }; + return { kind: 'found', name, primaryPath: candidate, primaryOrigin: 'agent' }; } } + // 4. Built-in skill served by the CLI, including legacy-name aliases. Last, + // so it answers for the names nothing on this machine claims: `core` and + // `wiki` live in the package, and the agent directory holds only the stub. + const served = await resolveServableSkill(name); + if (served.kind === 'blocked') return { kind: 'blocked', name: served.name, reason: served.reason }; + if (served.kind === 'found') { + return { kind: 'found', name: served.skill.name, primaryPath: served.skill.dir, primaryOrigin: 'builtin' }; + } + return null; } @@ -128,6 +251,12 @@ async function collectInstalledAgents( return matches; } +const PRIMARY_PATH_LABEL: Record<ResolvedSkill['primaryOrigin'], string> = { + team: 'Repo path ', + agent: 'Source path', + builtin: 'Package dir', +}; + interface SkillCard { name: string; source: SkillSource; @@ -136,7 +265,7 @@ interface SkillCard { contributors: string[]; tags: string[]; primaryPath: string; - primaryOrigin: 'team' | 'agent'; + primaryOrigin: ResolvedSkill['primaryOrigin']; installedIn: Array<{ agent: ResolvedAgent; path: string }>; } @@ -155,10 +284,16 @@ function printSkillCard(card: SkillCard): void { console.log(` Description : ${card.description || '(none)'}`); console.log(` Contributors : ${card.contributors.length > 0 ? card.contributors.join(', ') : '(none)'}`); console.log(` Tags : ${card.tags.length > 0 ? card.tags.join(', ') : '(none)'}`); - console.log(` ${card.primaryOrigin === 'team' ? 'Repo path ' : 'Source path'} : ${card.primaryPath}/`); + console.log(` ${PRIMARY_PATH_LABEL[card.primaryOrigin]} : ${card.primaryPath}/`); + if (card.primaryOrigin === 'builtin') { + console.log(` Read it with : teamai skill get ${card.name}`); + } if (card.installedIn.length === 0) { - console.log(' Installed in : (not installed in any agent yet)'); + // A served skill is never copied into an agent, so "yet" would be false. + console.log(card.primaryOrigin === 'builtin' + ? ' Installed in : (served by the CLI, not installed)' + : ' Installed in : (not installed in any agent yet)'); } else { const first = card.installedIn[0]; console.log(` Installed in : ${first.agent.id} (${first.path})`); diff --git a/src/skill-content.ts b/src/skill-content.ts new file mode 100644 index 00000000..99569b90 --- /dev/null +++ b/src/skill-content.ts @@ -0,0 +1,463 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import chalk from 'chalk'; +import { listFilesRecursive, pathExists } from './utils/fs.js'; +import { readSkillDescription } from './agent-skills.js'; +import { setStderrOnly } from './utils/logger.js'; + +// ─── CLI-served skill content ──────────────────────────── +// +// Built-in skill bodies ship inside the npm package and are +// printed on demand instead of being copied into every agent +// skills directory. What the agent reads therefore always +// matches the installed CLI version. +// +// npm package +// skills/<name>/SKILL.md deployed to agents (discovery stub) +// skill-data/<name>/SKILL.md never deployed, printed by `teamai skill get` +// +// Output discipline, mirrored from agent-browser: skill content +// goes to stdout untouched, every diagnostic goes to stderr, so a +// piped `teamai skill get <name> > SKILL.md` stays byte-exact. +// log.warn()/log.info() write to stdout outside hook mode, so this +// module writes its diagnostics with console.error directly. +// + +/** Placeholder replaced with the absolute skill directory when content is printed. */ +export const SKILL_DIR_PLACEHOLDER = '{SKILL_DIR}'; + +/** Directories inside a skill whose files `--full` appends, in this order. */ +const SUPPLEMENTARY_DIRS = ['references', 'templates'] as const; + +const SKILL_MD = 'SKILL.md'; + +/** + * Alternative names accepted by `skill get` / `skill path`. + * + * Legacy directory names are kept as aliases so that documentation, + * muscle memory and older team guides keep resolving after the content + * moves under skill-data/. + */ +const SKILL_ALIASES: Readonly<Record<string, string>> = { + default: 'core', + onboarding: 'setup', + join: 'setup', + codebase: 'wiki', + 'team-wiki-codebase': 'wiki', + learning: 'share', + learnings: 'share', + 'teamai-share-learnings': 'share', +}; + +/** + * Served skills that write to the team repo and need recall to be on. + * + * `share` publishes a session's learnings into the team's learnings branch, + * which is meaningful only when recall is enabled, and impossible against a + * read-only HTTP source. Before the discovery stub both gates were in + * deployment (`skipRecall`, `reportingOnly`) — the skill was simply absent. One + * stub routes to every workflow, so the gates moved here, where the command can + * also say why. + */ +const RECALL_DEPENDENT_SKILLS = new Set(['share']); + +/** Why a served skill is withheld right now. */ +export type SkillBlockReason = 'recall' | 'read-only' | 'config'; + +/** + * What makes this skill unusable right now, or null. + * + * Fails open only where there is no config at all: a fresh install reading + * the docs gets the content rather than a refusal it cannot act on. A config + * that exists but cannot be loaded blocks: whether recall is on, or the source + * writable, is then unknown, and the workflow would fail at `teamai contribute`. + */ +async function blockReason(name: string): Promise<SkillBlockReason | null> { + if (!RECALL_DEPENDENT_SKILLS.has(name)) return null; + const { NotInitializedError } = await import('./config.js'); + try { + const [{ autoDetectInit, findUnreadableProjectConfig }, { isRecallEnabled }] = await Promise.all([ + import('./config.js'), + import('./types.js'), + ]); + // Loading the config can migrate it and say so with `log.info`. That line + // must not land in the skill content or the JSON these commands print on + // stdout, so config loading reports on stderr for this one call. + const previous = setStderrOnly(true); + let loaded: Awaited<ReturnType<typeof autoDetectInit>>; + try { + // A broken project config is skipped by detection, which would then + // answer with the user config: another team's recall and source. + if (await findUnreadableProjectConfig()) return 'config'; + loaded = await autoDetectInit(); + } finally { + setStderrOnly(previous); + } + const { localConfig, teamConfig } = loaded; + // `teamai contribute` refuses a read-only source (read-only.ts), so the + // workflow would fail at its last step after the agent did all the work. + if (localConfig.repo?.kind === 'http') return 'read-only'; + return isRecallEnabled(localConfig, teamConfig) ? null : 'recall'; + } catch (e) { + return e instanceof NotInitializedError ? null : 'config'; + } +} + +/** A skill directory that ships inside the npm package. */ +export interface PackagedSkill { + name: string; + /** Absolute path of the skill directory. */ + dir: string; + /** True when this copy is the unit deployed into agent skills directories. */ + deployed: boolean; +} + +/** The two packaged roots: deployable units and CLI-served content. */ +export interface PackagedSkillRoots { + /** `skills/` — what `deployBuiltinSkills` copies into agents. */ + deployRoot: string; + /** `skill-data/` — never deployed, printed on demand. */ + dataRoot: string; +} + +/** + * Locate the packaged roots relative to this module. + * + * `realpathSync` first: a global `npm i -g` install exposes the CLI through a + * symlinked bin, and without resolving it `..` can land outside the package. + */ +export function packagedSkillRoots(): PackagedSkillRoots { + const modulePath = fileURLToPath(import.meta.url); + let moduleDir: string; + try { + moduleDir = path.dirname(fs.realpathSync(modulePath)); + } catch { + moduleDir = path.dirname(modulePath); + } + const packageRoot = path.join(moduleDir, '..'); + return { + deployRoot: path.join(packageRoot, 'skills'), + dataRoot: path.join(packageRoot, 'skill-data'), + }; +} + +async function readSkillDirs(root: string, deployed: boolean): Promise<PackagedSkill[]> { + let entries: string[]; + try { + entries = await fs.promises.readdir(root); + } catch { + return []; + } + + const skills: PackagedSkill[] = []; + for (const entry of entries.sort()) { + if (entry.startsWith('.')) continue; + const dir = path.join(root, entry); + if (await pathExists(path.join(dir, SKILL_MD))) { + skills.push({ name: entry, dir, deployed }); + } + } + return skills; +} + +/** Skills the CLI serves on demand: everything under skill-data/. */ +export async function listServableSkills(roots: PackagedSkillRoots = packagedSkillRoots()): Promise<PackagedSkill[]> { + return readSkillDirs(roots.dataRoot, false); +} + +/** + * Resolve a name or alias to a packaged skill. Servable content wins over the + * deployed stub, which stays reachable by its exact name for debugging. + */ +async function resolvePackagedSkill( + name: string, + roots: PackagedSkillRoots = packagedSkillRoots(), +): Promise<PackagedSkill | null> { + const servable = await listServableSkills(roots); + const deployed = await readSkillDirs(roots.deployRoot, true); + const candidates = [...servable, ...deployed.filter((s) => !servable.some((v) => v.name === s.name))]; + + const direct = candidates.find((s) => s.name === name); + if (direct) return direct; + + const aliased = SKILL_ALIASES[name]; + if (aliased) { + const match = candidates.find((s) => s.name === aliased); + if (match) return match; + } + return null; +} + +/** + * The outcome of asking for a skill by name. `blocked` carries the same + * information as `found`, minus the skill: a caller cannot print a directory it + * never received. + */ +export type ServableSkillResolution = + | { kind: 'found'; skill: PackagedSkill } + | { kind: 'blocked'; name: string; reason: SkillBlockReason } + | { kind: 'not-found'; name: string }; + +/** + * The only way to obtain a packaged skill outside this module. + * + * The recall and read-only gates are applied here, once, so every command that hands out a + * skill's content or its directory (`get`, `path`, `list`, `show`) inherits it + * by construction instead of remembering to check. + */ +export async function resolveServableSkill( + name: string, + roots: PackagedSkillRoots = packagedSkillRoots(), +): Promise<ServableSkillResolution> { + const skill = await resolvePackagedSkill(name, roots); + if (!skill) return { kind: 'not-found', name }; + const reason = await blockReason(skill.name); + if (reason) return { kind: 'blocked', name: skill.name, reason }; + return { kind: 'found', skill }; +} + +/** The two lines every command prints for a blocked skill. */ +export function blockMessage(name: string, reason: SkillBlockReason): { headline: string; hint: string } { + switch (reason) { + case 'recall': + return { + headline: `${name} needs recall, which is disabled for this team.`, + hint: 'Turn it on with `teamai recall enable`, or ask your team admin to enable sharing.', + }; + case 'read-only': + return { + headline: `${name} is not available: this team uses a read-only HTTP source, so nothing can be contributed from here.`, + hint: 'Ask a team admin to add the learning to the team repo.', + }; + case 'config': + return { + headline: `${name} is not available: the teamai config on this machine could not be loaded, so whether it can contribute is unknown.`, + hint: 'Run `teamai doctor` to see what is wrong with it, then try again.', + }; + default: { + const exhaustive: never = reason; + throw new Error(`Unhandled block reason ${String(exhaustive)}`); + } + } +} + +async function collectSupplementaryFiles(skillDir: string): Promise<Array<{ relativePath: string; content: string }>> { + const files: Array<{ relativePath: string; content: string }> = []; + + for (const dirName of SUPPLEMENTARY_DIRS) { + // listFilesRecursive walks nested directories and skips .pyc, __pycache__ and + // the rest of the repo's ignore list, which matters for the wiki's scripts/. + // Our references nest (references/methodology/, references/phases/), so a + // single-level scan would serve an incomplete skill. + const relativePaths = (await listFilesRecursive(path.join(skillDir, dirName))) + .map((relative) => `${dirName}/${relative}`) + .sort(); + + for (const relativePath of relativePaths) { + files.push({ + relativePath, + content: await fs.promises.readFile(path.join(skillDir, relativePath), 'utf8'), + }); + } + } + + return files; +} + +function withTrailingNewline(text: string): string { + return text.endsWith('\n') ? text : `${text}\n`; +} + +/** + * Render a packaged skill exactly as the agent should read it: the raw + * SKILL.md including frontmatter, with {SKILL_DIR} resolved to the absolute + * packaged directory so that documented script invocations can be run as-is. + */ +export async function renderSkill(skill: PackagedSkill, options: { full?: boolean } = {}): Promise<string> { + const resolve = (text: string): string => text.split(SKILL_DIR_PLACEHOLDER).join(skill.dir); + + let out = withTrailingNewline(resolve(await fs.promises.readFile(path.join(skill.dir, SKILL_MD), 'utf8'))); + + if (options.full) { + for (const file of await collectSupplementaryFiles(skill.dir)) { + out += `\n--- ${file.relativePath} ---\n\n`; + out += withTrailingNewline(resolve(file.content)); + } + } + + return out; +} + +/** Diagnostics never share stdout with skill content. */ +function diagnostic(line: string): void { + console.error(line); +} + +function notFound(name: string, available: PackagedSkill[]): void { + diagnostic(`${chalk.red('✖')} Skill not found: ${name}`); + diagnostic(` Available: ${available.map((s) => s.name).join(', ')}`); + diagnostic(' Run `teamai skill list` to see what the installed CLI serves.'); + process.exitCode = 1; +} + +function rootsMissing(): void { + diagnostic(`${chalk.red('✖')} Packaged skill content not found.`); + diagnostic(' The installed teamai-cli package looks incomplete; reinstall with `npm i -g teamai-cli`.'); + process.exitCode = 1; +} + +/** + * Refuse a blocked skill. Every command that hands out a skill's content or its + * directory goes through here, so an agent is routed away from a workflow that + * cannot finish whichever way it asks. A routing aid, not access control: the + * files ship in the npm package either way. + */ +function refuseBlocked(name: string, reason: SkillBlockReason): void { + const { headline, hint } = blockMessage(name, reason); + diagnostic(`${chalk.red('✖')} ${headline}`); + diagnostic(` ${hint}`); + process.exitCode = 1; +} + +export interface SkillGetOptions { + full?: boolean; + all?: boolean; +} + +/** + * `teamai skill get <name...> [--full] [--all]` — print version-matched skill + * content to stdout. + */ +export async function skillGet(names: string[], options: SkillGetOptions = {}): Promise<void> { + const roots = packagedSkillRoots(); + const servable = await listServableSkills(roots); + + if (servable.length === 0) { + rootsMissing(); + return; + } + + // An unknown flag is forgiven — a hallucinated flag should not cost the agent a + // round-trip — but an unknown name is fatal: the agent would act on the wrong + // instructions. Commander hands unknown options through as operands here. + const requested: string[] = []; + for (const name of names) { + if (name.startsWith('-')) { + diagnostic(`${chalk.yellow('⚠')} Unknown flag ignored: ${name}`); + continue; + } + requested.push(name); + } + + const targets: PackagedSkill[] = []; + if (options.all) { + // The gate holds for the inventory dump too: a blocked skill is left out + // and named on stderr, the rest is still served. + for (const listed of servable) { + const resolved = await resolveServableSkill(listed.name, roots); + // The listing and the resolver read one catalog, so a listed name is + // either found or blocked. + if (resolved.kind !== 'found') { + if (resolved.kind === 'blocked') { + const { headline, hint } = blockMessage(listed.name, resolved.reason); + diagnostic(`${chalk.yellow('⚠')} Skipped ${listed.name}. ${headline} ${hint}`); + } + continue; + } + targets.push(resolved.skill); + } + } else { + for (const name of requested) { + const resolved = await resolveServableSkill(name, roots); + if (resolved.kind === 'not-found') { + notFound(name, servable); + return; + } + if (resolved.kind === 'blocked') { + refuseBlocked(resolved.name, resolved.reason); + return; + } + targets.push(resolved.skill); + } + } + + if (targets.length === 0) { + diagnostic(`${chalk.red('✖')} No skill name provided. Usage: teamai skill get <name> [--full], or --all`); + diagnostic(` Available: ${servable.map((s) => s.name).join(', ')}`); + process.exitCode = 1; + return; + } + + const rendered: string[] = []; + for (const skill of targets) { + rendered.push(await renderSkill(skill, { full: options.full })); + } + process.stdout.write(rendered.join('\n---\n\n')); +} + +/** + * `teamai skill path <name>` — print the packaged directory, for agents that + * read files directly or need to run the scripts a skill ships. + * + * A name is required, and a blocked skill gets the same refusal as `skill get`, + * so an agent asking for the directory is routed the same way. + */ +export async function skillPath(name: string): Promise<void> { + const roots = packagedSkillRoots(); + + const resolved = await resolveServableSkill(name, roots); + switch (resolved.kind) { + case 'not-found': + notFound(name, await listServableSkills(roots)); + return; + case 'blocked': + refuseBlocked(resolved.name, resolved.reason); + return; + case 'found': + console.log(resolved.skill.dir); + return; + default: { + const exhaustive: never = resolved; + throw new Error(`Unhandled resolution ${String(exhaustive)}`); + } + } +} + +/** + * One catalog entry, as `teamai skill list --json` reports it. + * + * A skill the recall gate blocks is still listed, so the agent learns it exists + * and what to turn on, but its directory is withheld like `skill path` does. + */ +interface SkillCatalogEntryFields { + name: string; + description: string; + deployed: boolean; +} + +/** + * `blockedBy` carries the directory with it: a blocked entry has no path to + * report, and a served one always has. Both variants keep the `path` key so the + * JSON shape does not change with the gate. + */ +export type SkillCatalogEntry = + | (SkillCatalogEntryFields & { blockedBy: null; path: string }) + | (SkillCatalogEntryFields & { blockedBy: SkillBlockReason; path: null }); + +export async function skillCatalog(roots: PackagedSkillRoots = packagedSkillRoots()): Promise<SkillCatalogEntry[]> { + const skills = await listServableSkills(roots); + const entries: SkillCatalogEntry[] = []; + for (const skill of skills) { + const resolved = await resolveServableSkill(skill.name, roots); + const fields: SkillCatalogEntryFields = { + name: skill.name, + description: await readSkillDescription(path.join(skill.dir, SKILL_MD)), + deployed: skill.deployed, + }; + entries.push(resolved.kind === 'blocked' + ? { ...fields, blockedBy: resolved.reason, path: null } + : { ...fields, blockedBy: null, path: skill.dir }); + } + return entries; +} diff --git a/src/source.ts b/src/source.ts index a2cddb16..ddf083d3 100644 --- a/src/source.ts +++ b/src/source.ts @@ -19,7 +19,7 @@ import { import { getHandler } from './resources/index.js'; import { ResourceHandler } from './resources/base.js'; import { resolveSkillDestination } from './resources/skills.js'; -import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; +import { BUILTIN_SKILL_NAMES, LEGACY_BUILTIN_SKILL_NAMES } from './builtin-skills.js'; import { getUserHome } from './utils/home.js'; import { assertSafeResourceName, assertWithinRoot } from './utils/path-safety.js'; import type { @@ -623,8 +623,9 @@ async function getLocalTeamSkillNames(teamConfig: TeamaiConfig, localConfig: Loc const handler = getHandler('skills'); const items = await handler.scanTeamForPull(teamConfig, localConfig); const names = new Set(items.map((i) => i.name)); - // Also include builtin skills - for (const name of BUILTIN_SKILL_NAMES) { + // Also include builtin skills, legacy ones too: a source-team removal must not + // delete a legacy tree wholesale, which only pull's ownership rule may prune. + for (const name of [...BUILTIN_SKILL_NAMES, ...LEGACY_BUILTIN_SKILL_NAMES]) { names.add(name); } return names; diff --git a/src/types.ts b/src/types.ts index 6dffa604..f1b8cade 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,8 +85,8 @@ export const SharingConfigSchema = z.object({ // Optional (not .default) so existing TeamaiConfig literals stay valid; use // isContributeHintEnabled() for the resolved view. contributeHint: z.object({ - /** Team default: whether the Stop hook nudges members to run - * /teamai-share-learnings after a high-friction session. Teams that route + /** Team default: whether the Stop hook nudges members towards the + * share workflow after a high-friction session. Teams that route * knowledge sharing through their own review flow can turn the nudge off * without disabling the rest of the Stop hook (update check, votes sync, * dashboard reporting). */ diff --git a/src/uninstall.ts b/src/uninstall.ts index 69781ef7..e3796095 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -38,7 +38,17 @@ import { agentStemFromFilename } from './resources/agent-format.js'; import { resolveDocsDestination } from './resources/docs.js'; import { listTeamAgentDirs } from './resources/agents.js'; import { BUILTIN_AGENT_NAMES } from './builtin-agents.js'; -import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; +import { + BUILTIN_SKILL_NAMES, + LEGACY_BUILTIN_SKILL_NAMES, + ownedSkillFiles, + isCliOwnedSkillName, + prunedWhole, + removeOwnedFiles, + skillsGuardBase, +} from './builtin-skills.js'; +import { getHermesHome } from './hermes-home.js'; +import { CODEX_TOOL, SHARED_AGENT_SKILLS_PATH } from './resources/skills.js'; import { pathExists, readFileSafe, @@ -83,8 +93,11 @@ interface RemovalPlan { hookManifestPath: string; /** CLAUDE.md files with teamai rules blocks. */ claudeMdFiles: string[]; - /** Skill directories synced from team repo. */ - skillDirs: string[]; + /** + * Skill directories synced from team repo, each with the base directory its + * skills root hangs off: the prune refuses a link anywhere below that base. + */ + skillDirs: SkillDirEntry[]; /** Rule .md files synced from team repo (plus CLI built-in rules). */ ruleFiles: string[]; /** Built-in agent .md files deployed by the CLI (e.g. teamai-recall). */ @@ -108,6 +121,12 @@ interface RemovalPlan { } /** Per-tool findings collected during discovery (tool-specific resources only). */ +/** A skill directory to remove, and the base the link guard starts from. */ +interface SkillDirEntry { + dir: string; + baseDir: string; +} + interface ToolResources { hookFiles: Array<{ path: string; tool: string; manifestPath: string }>; openclawHookDirs: Array<{ hooksDir: string; tool: string }>; @@ -115,7 +134,7 @@ interface ToolResources { ompHookFile: string | null; dshHookFile: string | null; claudeMdFiles: string[]; - skillDirs: string[]; + skillDirs: SkillDirEntry[]; ruleFiles: string[]; agentFiles: string[]; } @@ -235,6 +254,8 @@ async function discoverToolResources( tool: string, toolPath: TeamaiConfig['toolPaths'][string], baseDir: string, + /** Home, or the project root: where the skills link guard starts (`skillsGuardBase`). */ + scopeRoot: string, teamSkillNames: Set<string>, teamRuleNames: Set<string>, teamAgentNames: Set<string>, @@ -344,17 +365,36 @@ async function discoverToolResources( // (c) Skills — only those matching team repo if (toolPath.skills) { - const skillRoots = new Set([path.join(baseDir, toolPath.skills)]); + // Skills root → the base the link guard starts from. + const configuredSkills = path.join(baseDir, toolPath.skills); + const skillRoots = new Map([[configuredSkills, skillsGuardBase(scopeRoot, configuredSkills)]]); + // OpenClaw and Hermes receive skills where team sync and the stub put them + // (`skillsDirForTool`): the workspace, and HERMES_HOME. if (tool === 'openclaw') { const workspaceDir = await resolveOpenclawWorkspaceDir(); - if (workspaceDir) skillRoots.add(path.join(workspaceDir, 'skills')); + if (workspaceDir) { + const workspaceSkills = path.join(workspaceDir, 'skills'); + skillRoots.set(workspaceSkills, skillsGuardBase(scopeRoot, workspaceSkills)); + } + } + if (tool === 'hermes') { + const hermesSkills = path.join(getHermesHome(), 'skills'); + skillRoots.set(hermesSkills, skillsGuardBase(scopeRoot, hermesSkills)); } - for (const skillsDir of skillRoots) { + // `resolveSkillDestination` writes Codex's copy into the shared + // .agents/skills root whenever that skill already lives there, so uninstall + // must look where deployment could have put it — the legacy prune already + // does. Codex only: another tool's pass must not reach into it. + if (tool === CODEX_TOOL) { + const sharedSkills = path.join(baseDir, SHARED_AGENT_SKILLS_PATH); + skillRoots.set(sharedSkills, skillsGuardBase(scopeRoot, sharedSkills)); + } + for (const [skillsDir, rootBase] of skillRoots) { if (await pathExists(skillsDir)) { const dirs = await listDirs(skillsDir); for (const dir of dirs) { if (teamSkillNames.has(dir)) { - res.skillDirs.push(path.join(skillsDir, dir)); + res.skillDirs.push({ dir: path.join(skillsDir, dir), baseDir: rootBase }); } } } @@ -417,6 +457,9 @@ async function buildRemovalPlan( const repoPath = localConfig.repo.localPath; const teamSkillNames = await collectTeamSkillNames(repoPath); for (const name of BUILTIN_SKILL_NAMES) teamSkillNames.add(name); + // Directories earlier releases deployed: uninstall would otherwise leave the + // pre-stub skill trees behind on any machine that upgraded. + for (const name of LEGACY_BUILTIN_SKILL_NAMES) teamSkillNames.add(name); const teamRuleNames = await collectTeamRuleNames(repoPath); for (const name of BUILTIN_RULE_NAMES) teamRuleNames.add(name); const teamAgentNames = await collectTeamAgentNames(repoPath); @@ -462,6 +505,7 @@ async function buildRemovalPlan( tool, toolPath, resolveToolBaseDir(tool, localConfig), + resolveBaseDir(localConfig), teamSkillNames, teamRuleNames, teamAgentNames, @@ -667,8 +711,13 @@ function printSummary(plan: RemovalPlan, agentFilter?: string): void { if (plan.skillDirs.length > 0) { console.log(` Skills (${plan.skillDirs.length} directories):`); - for (const skillDir of plan.skillDirs) { - console.log(` ${skillDir}`); + for (const { dir: skillDir } of plan.skillDirs) { + // A CLI-owned directory loses the files TeamAI packaged, not whatever the + // member added beside them, so the prompt must not promise the directory. + const suffix = isCliOwnedSkillName(path.basename(skillDir)) + ? ' (TeamAI-packaged files only; anything you added stays)' + : ''; + console.log(` ${skillDir}${suffix}`); } console.log(''); } @@ -829,16 +878,53 @@ async function executeRemoval(plan: RemovalPlan): Promise<void> { } } - // (c) Remove synced skills - for (const skillDir of plan.skillDirs) { + // (c) Remove synced skills. + // + // A team-repo skill is synced whole, so the whole directory goes. A CLI-owned + // one is not: deployment writes only the files in PACKAGED_SKILL_FILES and + // never touched a file a member added beside them, so uninstall removes those + // same paths and keeps the rest — the same ownership rule pull applies. + // Deleting the directory here would undo the guarantee one command over. + // + // Pull's archive is deliberately not applied: there the member is upgrading + // and did not ask for anything to go, here they asked for all of it. Leaving + // copies behind would be the thing they ran the command to avoid. + let removedSkillDirs = 0; + const keptSkillDirs: string[] = []; + const linkedSkillDirs: string[] = []; + const failedSkillDirs: { skillDir: string; first: { file: string; error: string } }[] = []; + for (const { dir: skillDir, baseDir } of plan.skillDirs) { try { - await remove(skillDir); + const name = path.basename(skillDir); + if (isCliOwnedSkillName(name)) { + const result = await removeOwnedFiles(skillDir, await ownedSkillFiles(name), baseDir); + if (prunedWhole(result)) removedSkillDirs++; + else if (result.skippedSymlink) linkedSkillDirs.push(skillDir); + // A delete that failed is not a member's file: say what happened, not + // "the packaged files were removed". + else if (result.notRemoved.length > 0) failedSkillDirs.push({ skillDir, first: result.notRemoved[0] }); + else keptSkillDirs.push(skillDir); + } else { + await remove(skillDir); + removedSkillDirs++; + } } catch (e) { log.warn(`Failed to remove skill ${skillDir}: ${(e as Error).message}`); } } - if (plan.skillDirs.length > 0) { - log.success(`Removed ${plan.skillDirs.length} skill directories`); + if (removedSkillDirs > 0) { + log.success(`Removed ${removedSkillDirs} skill directories`); + } + for (const skillDir of keptSkillDirs) { + log.warn(`Kept ${skillDir}: it holds files TeamAI did not put there. The packaged files were removed; delete the rest yourself once you have saved what you need.`); + } + // A different reason, so a different sentence: nothing here was touched, and + // "delete the rest yourself" would send the member into the link target. + for (const skillDir of linkedSkillDirs) { + log.warn(`Kept ${skillDir}: it is reached through a symlink, so TeamAI left it and whatever the link points at alone.`); + } + for (const { skillDir, first } of failedSkillDirs) { + log.warn(`Could not delete packaged files under ${skillDir}. First: ${first.file} — ${first.error}. Fix the permissions and run \`teamai uninstall\` again, or delete the directory yourself.`); } // (d) Remove synced rules diff --git a/src/utils/logger.ts b/src/utils/logger.ts index b261c473..aba89b09 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -115,10 +115,13 @@ export function setSilent(s: boolean): void { /** * Route non-error log output to stderr. Used by hook-dispatch commands to - * keep stdout as a clean JSON channel for the AI tool. + * keep stdout as a clean JSON channel for the AI tool. Returns the previous + * mode, so a caller that needs it for one step can put it back. */ -export function setStderrOnly(s: boolean): void { +export function setStderrOnly(s: boolean): boolean { + const previous = stderrMode; stderrMode = s; + return previous; } /** Write a "non-error" log line. Goes to stderr in hook mode, stdout otherwise. */ diff --git a/src/wiki-engine/manifest-schema.ts b/src/wiki-engine/manifest-schema.ts index ac0f3b9e..a0d5647b 100644 --- a/src/wiki-engine/manifest-schema.ts +++ b/src/wiki-engine/manifest-schema.ts @@ -1,8 +1,9 @@ /** * Codebase output manifest schema definitions. * - * The manifest is the contract between AI compilers (e.g. team-wiki-codebase - * Skill) and the deterministic Node-side compiler (`compileFromManifest`). + * The manifest is the contract between AI compilers (e.g. the `wiki` skill, + * served by `teamai skill get wiki`) and the deterministic Node-side compiler + * (`compileFromManifest`). * * Two versions are supported: *