Skip to content

feat(skill): serve builtin skill content from the CLI, deploy a discovery stub - #699

Merged
jeff-r2026 merged 37 commits into
Tencent:mainfrom
SaulMoro:feat/skill-get-stub
Sep 23, 2026
Merged

jeff-r2026 merged 37 commits into
Tencent:mainfrom
SaulMoro:feat/skill-get-stub

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Closes #678. Closes #730.

What #730 asked for, and what this does instead

#730 reports that BUILTIN_SKILL_NAMES does not hold teamai, so scanLocalForPush reads the deployed stub as a user-authored skill and teamai push uploads it to the team repo. Pull cleanup, uninstall and classification take the wrong branch for the same reason.

Here the set is {'teamai'}, and the push scan asks isCliOwnedSkillName (src/resources/skills.ts:443), which covers the current name and the legacy ones a pre-stub release wrote. A member who upgrades and runs teamai push --all before their next pull still has those trees on disk, and the scan skips them too.

Two differences from the issue's plan are worth stating.

teamai-workflow and teamai-import leave the set. The package has never shipped either name, so a directory with one is the member's own skill, and guarding it stopped them publishing it. push --all --dry-run now offers them, which the generic git provider row below records.

The delivery half of #730 stops being a delivery problem. The issue accepts "takes effect on the next pull, one session behind" because the content is a file the CLI copies. Here the content is not copied at all. teamai skill get core prints what the running binary packages, so an upgrade changes the answer immediately and there is nothing to drift. What the next pull still does is rewrite the 2.5 KB stub and remove the trees earlier releases left, which pull.ts:966 runs even when the team repo SHA has not moved.

Summary

The built-in skill content is now 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 the moment that version is installed — no pull, no per-agent copy that drifts between releases. The one thing npm i -g teamai-cli@latest does not do by itself is rewrite what is already on disk in the agent directories: the deployed stub and the prune of the legacy trees ride the next deployBuiltinSkills, which pull.ts:966 runs even when the team repo SHA is unchanged, and which the session-start hook init injects runs automatically. So a member upgrading mid-session keeps a stale stub until their next session, while the content the stub loads is already the new version's. Agents get one deployable file, a discovery stub that points at those commands.

 npm package
 skills/
-├── team-wiki-codebase/     SKILL.md + references/ + scripts/   copied to every agent
-├── teamai-share-learnings/ SKILL.md                            copied to every agent
-└── teamai/                 SKILL.md + 6 references             copied to every agent
+└── teamai/SKILL.md         2.5 KB discovery stub, the only unit deployed
+skill-data/                 never deployed, printed by `teamai skill get`
+├── core/                   daily routing, pull/push/status/doctor, generated command reference
+├── setup/                  day 0 and repo lifecycle
+├── wiki/                   codebase knowledge base, incl. scripts/
+└── share/                  session learnings

What the agent reads, and when:

session start                  stub description            0.9 KB   always in context
task matches                   stub body                   1.5 KB   holds the commands
teamai skill get core          daily workflow              6.5 KB   on demand
teamai skill get core --full   + commands.md, contribute-member, troubleshooting  36 KB   on demand
teamai skill get wiki          architecture workflow        19 KB   on demand
teamai skill get wiki --full   + 14 references             132 KB   on demand

Commands added to the existing skill group, no new top-level command:

teamai skill get [names...] [--full] [--all]  # core | setup | wiki | share, legacy names alias
teamai skill path <name>                      # packaged directory, for the scripts a skill ships
teamai skill list --json                      # the catalog the installed CLI serves

Deployment writes one file and clears what it does not own:

 deployBuiltinSkills
   for each installed, non-excluded agent
+    resolve its skills directory as team-skill sync does (skillsDirForTool:
+      OpenClaw's workspace, HERMES_HOME), not base dir + configured path
+    refuse any skill root that is a symlink: none of what it points at is ours
+    archive, then remove, the files earlier releases packaged under
+      team-wiki-codebase and teamai-share-learnings, then the directories left empty
+    archive, then remove, the seven teamai/references/*.md a pre-stub release
+      wrote, by name — never SKILL.md, which is shipped now, not retired
-    copy skills/<name>/ whole tree, overwrite: true
-    ensureSkillFrontmatter(...)
+    copy skills/teamai/SKILL.md verbatim

Thirty-seven commits, each green: the commands, the move plus the stub, the content repairs, the prune with the run-time recall gate, the docs, one commit per review pass, the rebase onto #724 (which moves its new provider-tgit.md into skill-data/setup/references/ and repoints the two files that cite it), and three rounds on the prune — the archive, then fail-closed on a failed copy and collision-free paths, then the symlink refusal and archiving only what is retired, and two commits after the rebase: the reviewer's round on a66f025, and a pre-push review of the whole branch (below); then the rounds on e4aa6ca, df6f018, b4f675f, fe0540e, 18c26b8, e747a75, 238e205, f16e725 and 427f7e2 (below).

Rebased onto origin/main at 667aed0 (#715, #698, #689, #741, #733, #737 landed since a52374a). One conflict, in the closing lines of init (src/init.ts), replayed at two commits: #689 reworded the hooks line to Skills, rules, env and docs auto-sync on each session start when the selected agent has active TeamAI hooks., and this branch replaces the Built-in skills (e.g. team-wiki-codebase) line with the stubDeployed check. The resolution keeps both. Two things #698 changed underneath the skills without a textual conflict, each folded into the commit it belongs to so every commit stays green (git rebase -x 'npx tsc --noEmit && npx vitest run commands-reference' passes on all 37): the push --role/--project help text, so skill-data/core/references/commands.md is regenerated in the commit that introduces it; and push-namespace-e2e.test.ts, whose "newer namespaced rule" case relied on #730's bug — its second push uploaded the deployed teamai stub to skills/fe-skills/teamai, and the ${branch}:rules/… assertion only held because that push created a branch. With the stub CLI-owned there is nothing to push, so the case now asserts No new or modified resources to push and that main keeps the teammate's rule; on main that assertion fails, which makes it a #730 regression guard too. #698's new paragraph in contribute-member.md followed the move to skill-data/core/references/ by itself. Earlier: rebased onto a52374a (#718, #713, #736, #739 landed since cd3e0e6). #713 and #736 edited skills/teamai/references/*.md, which this branch moves to skill-data/setup/references/; the two hunks that did not follow the move (the TGIT_TOKEN note in join-member.md, #736's share hand-off in setup-admin.md, the latter in English as the served text is) are carried over in 96d0f7c, together with #739's partial config.js mock. Commit hashes named in the review rounds below predate this rebase. The previous rebase was onto cd3e0e6 (#720, #700, #695, #729 landed since the previous base). One conflict, in buildHint (src/contribute-check.ts): #720 dropped the Print the following message verbatim wrapper from the Stop-hook hint and moved the relay to relayWhenHidden; this branch had changed the sentence inside it to name /teamai and teamai skill get share. The resolution keeps #720's shape with this branch's wording. Nothing in those four commits touches skills/ or skill-data/; what they change and whether the served skills had to follow is recorded under What main changed underneath the skills below.

Evidence

Version match: the served content is the packaged content of the running binary, byte for byte (skill get teamai | diff - skills/teamai/SKILL.md below, and the e2e serves every skill it lists for all four workflows). Before this change an upgrade left the previous release's instructions in every agent until the next pull, which is what the four drift-fix commits were about.

Before: 21 files, 176 775 bytes copied into every installed agent's skills directory on every pull. A machine with ten agents held ten copies. team-wiki-codebase/SKILL.md was 38 705 bytes, read in full whenever the skill triggered.

After: one file of 2 529 bytes per agent, byte-identical to the packaged stub. Measured with the built CLI:

$ find skills -type f
skills/teamai/SKILL.md
$ wc -c skills/teamai/SKILL.md
2529

$ node dist/index.js skill get teamai | diff - skills/teamai/SKILL.md && echo IDENTICAL
IDENTICAL

$ node dist/index.js skill get wiki --full | grep -c '^--- '
14

The recall gate, the stream discipline and the exit codes, from the same build:

$ node dist/index.js skill get share
✖ share needs recall, which is disabled for this team.
  Turn it on with `teamai recall enable`, or ask your team admin to enable sharing.
$ echo $?
1

$ node dist/index.js skill get --all               # exit 0, share left out and named on stderr
⚠ Skipped share. share needs recall, which is disabled for this team. Turn it on with `teamai recall enable`, …
$ node dist/index.js skill path share               # exit 1, same refusal as `skill get share`
$ node dist/index.js skill list --json | jq '.skills[] | select(.name=="share") | {blockedBy, path}'
{ "blockedBy": "recall", "path": null }

# a read-only HTTP team, recall on: `teamai contribute` cannot write there, so share is withheld too
$ node dist/index.js skill get share                # exit 1
✖ share is not available: this team uses a read-only HTTP source, so nothing can be contributed from here.
  Ask a team admin to add the learning to the team repo.

$ node dist/index.js skill get no-such-skill        # stdout empty, exit 1
✖ Skill not found: no-such-skill
  Available: core, setup, share, wiki

$ node dist/index.js skill get core --bogus         # exit 0, content still served
⚠ Unknown flag ignored: --bogus

{SKILL_DIR} resolves at print time, so the wiki's scripts run as written:

$ node dist/index.js skill get wiki --full | grep -m1 'python3 .*scan_repo.py'
python3 /path/to/teamai-cli/skill-data/wiki/scripts/scan_repo.py <project_root> --depth 2 --top 10
$ node dist/index.js skill get wiki --full | grep -c '{SKILL_DIR}'
0
$ python3 "$(node dist/index.js skill path wiki)/scripts/validate_kb.py" --help
usage: validate_kb.py [-h] [--verbose] kb_dir

Automated runs, on the head revision (5f5b2c1, on origin/main at 667aed0):

npx tsc --noEmit         clean
npm run build            ok
npx vitest run           4206 passed, 1 skipped, 285 files, 0 failures
npm run test:e2e         224 passed, 26 skipped, 44 files (3 skipped), 0 failures, no retries
                         (the whole e2e suite, including #698's push-namespace-e2e.test.ts;
                         src/__tests__/e2e/skill-serving-cli.test.ts is 7 of those)

The e2e suite was run locally because CI cannot run it here. E2E (GitHub provider, full surface) carries if: ${{ vars.TEAMAI_TEST_REPO_URL != '' }} (ci.yml:92), so it skips on every
fork PR — the green CI check on this PR contains no e2e at all. npm run test:e2e above is that
job's suite, run against the built CLI on this revision. One caveat worth recording: the run needs
npm ci first, since opencode-recall.test.ts shells out to node_modules/opencode-ai/postinstall.mjs
and fails in beforeAll without it.

Three guards were added because the failures they catch are invisible otherwise:

commands-reference.test.ts    renders references/commands.md from the Commander table and diffs it
skill-commands-exist.test.ts  resolves every `teamai …` string in skill-data against that table
                              (carries a case proving it catches `teamai extract graph`)
skill-content.test.ts         asserts through `npm pack` that skills/ and skill-data/ ship

The second one is what would have stopped e151d43, 1ca43ac, 8bb0548 and 2ddb546, each of which fixed a skill naming a command the CLI no longer had. The third catches a missing package.json "files" entry, which every other test passes through while the published package serves nothing.

An audit of the three skills found 60 defects; this fixes the ones that survive the move. The wiki skill advertised teamai extract graph, which has never existed. The cheat sheet labelled "ground truth" omitted 19 real commands. Every documented script path assumed the skill sat in the agent's own directory.

Language: instructions in English, generated documents unchanged

The instructions the CLI prints are English, because the CLI now prints them and the repo's rule is CLI user-facing output must be English. No Chinese in production code. The documents those instructions produce keep the language main produces: share still requires Simplified Chinese learnings (main: 所有生成的文档必须使用中文撰写), and wiki still writes its knowledge base in Simplified Chinese, which main did implicitly by being written in Chinese. Both are now English sentences that say so. An earlier revision moved both to the user's session language; the reviewer flagged that as a product change this PR does not need, and it is reverted (round on 238e205 below).

Everything under skills/ and skill-data/ is English, including the wiki skill, which main shipped in Chinese; the only non-English left is the \uXXXX escapes in validate_kb.py's compatibility patterns. validate_kb.py still recognises the headings of knowledge bases built by the previous release, so --update and validation keep working on them; new documents are generated with English names ({project_name} Technical Architecture.md, XX_{component}_Design.md, G1_…_Component_Dependency_Matrix.md).

Manual matrix, built CLI against a real GitHub team repo (a throwaway private repo, deleted afterwards), in an isolated HOME with Claude, Codex, CodeBuddy and OpenCode all present:

init --agent claude,codex,codebuddy,opencode
  .claude/skills              teamai/SKILL.md
  .codex/skills               teamai/SKILL.md
  .codebuddy/skills           teamai/SKILL.md
  .config/opencode/skills     teamai/SKILL.md      one file each, nothing else

seed the pre-stub layout in .claude and .codebuddy
  team-wiki-codebase/SKILL.md, team-wiki-codebase/references/phase0.md,
  teamai-share-learnings/SKILL.md, teamai/references/setup-admin.md
pull
  .claude/skills              teamai/SKILL.md      legacy trees and the stale
  .codebuddy/skills           teamai/SKILL.md      references both gone

diff deployed packaged           IDENTICAL
skill list --json                core, setup, share, wiki
skill get core | setup | wiki    6 383 | 5 553 | 19 088 bytes
skill path wiki                  scripts run: validate_kb.py --help, scan_repo.py --help
recall disable → skill get share exit 1, "share needs recall, which is disabled for this team"
recall enable  → skill get share serves the skill
uninstall --force                removed 4 skill directories, agent dirs left empty

A fresh team has recall off by default (getRecallSharing returns false), so skill get share refuses until someone enables it. That matches the behaviour it replaces: the old deployment skipped the share skill under skipRecall.

Generic git provider row, re-run on 1d84ec3, same matrix, built CLI in an isolated HOME against a local bare remote (provider: git in teamai.yaml, the layout the repo's own e2e tests use), Claude, Codex, CodeBuddy and OpenCode present, plus user-created teamai-workflow and teamai-import skills seeded in .claude/skills:

pull                               one teamai/SKILL.md per agent, legacy trees and stale references gone
                                   teamai-workflow, teamai-import still "# mine" (user-owned, never touched)
diff deployed packaged             IDENTICAL
recall off   skill get share       exit 1  |  skill get --all  serves core, setup, wiki; stderr "Skipped share"
             skill path share      exit 1  |  skill list --json  share: blockedBy "recall", path null
recall enable                      all four served by --all; skill path share prints the directory
skill get core                     byte-identical bar {SKILL_DIR}; commands.md shows `skill get [names...]`
uninstall --force                  8 skill dirs removed; team-wiki-codebase kept and named because it
                                   holds the member's references/methodology/my-notes.md, which
                                   survives with teamai-workflow

After the rebase onto 667aed0, the generic git row was re-run on 5f5b2c1 (built CLI, isolated HOME, local bare remote, user scope, Claude, Codex, CodeBuddy and OpenCode enabled; the pre-stub layout, from v0.25.0, seeded in .claude and .codebuddy after the first pull):

first pull                one teamai/SKILL.md per agent, nothing else
second pull               "Already synced, skipping" — legacy trees and stale references still cleared,
                          the "# old stub" rewritten; IDENTICAL to packaged in all four
skill list --json         core, setup, share (blockedBy "recall"), wiki
skill get share           exit 1, "share needs recall, which is disabled for this team"
push --all --dry-run      offers teamai-workflow only (the member's); no CLI-owned tree
skill path wiki           validate_kb.py --help runs
uninstall --force         4 skill dirs removed; teamai-workflow remains

init's closing lines, the conflict above, were not re-run against a hosted provider: init takes no local remote, and no test asserts those lines.

Reporting-only HTTP row, built CLI, isolated HOME, teamai init --http … --token …, Claude and
Codex installed, the pre-stub layout seeded. This is the row the last review pass was about:

parent revision (e18fd1b)   pull leaves ~/.claude/skills/teamai/SKILL.md at "# old stub" and
                            references/setup-admin.md at "# old ref", forever
this revision (1d84ec3)     pull deploys teamai/SKILL.md to .claude and .codex, IDENTICAL to
                            packaged, and clears the stale references; skill get wiki serves
                            with no team repo on disk

Qoder CN row, run on a66f025 after the rebase, because #695 added qoder-cn as a built-in target with its own user root (~/.qoder-cn/skills) and the prune had never seen it. Built CLI, isolated HOME, local bare remote (provider: git), init --agent claude,qoder-cn --scope user, the pre-stub trees seeded in both roots plus one file the package never shipped (team-wiki-codebase/references/phase0.md) to stand in for a member's own:

init                     .claude/skills/teamai/SKILL.md and .qoder-cn/skills/teamai/SKILL.md, nothing else
pull                     teamai-share-learnings gone, teamai/references/setup-admin.md gone, in both roots
                         team-wiki-codebase kept and named in both, because it holds the member's file;
                         its packaged SKILL.md removed, archived under
                         ~/.teamai/removed-skills/<run>/<base>/qoder-cn/.qoder-cn-skills/…
diff deployed packaged   IDENTICAL for .claude and for .qoder-cn
recall off               skill get share exit 1 | skill get --all exit 0, stderr "Skipped share"
push --all --dry-run     nothing offered: neither the stub nor the legacy trees are scanned as the member's
uninstall --force        4 skill dirs removed; team-wiki-codebase kept and named in both roots

Round on a66f025, and the pre-push review that followed

The reviewer's four findings on a66f025: the nudge said bare /teamai, which the core skill answers with a menu and a stop, so it now names /teamai share what this session taught me with the literal command kept; the setup skill routed "publish one skill" to share, which handles session learnings and is refused when recall is off, so that row and the two references that repeated it point at contribute-member.md; classifySkill used BUILTIN_SKILL_NAMES alone and showed the legacy trees as [local-only] until the first pull, so it uses isCliOwnedSkillName like push and uninstall do; the Chinese trigger keywords stayed that round and went in the next (below).

Then a review of the whole branch against #678, #730, the design doc and the repo rules, run before pushing so the next automated round has less to find. What it turned up, and what was done:

  • The nudge fired on teams where the command it names refuses. contributeHintAllowed checked the hint switch only; recall is off by default, and teamai skill get share exits 1 then. The hint is now withheld while recall is off, the same gate the workflow has. This is the one product call in the set, so it is stated as one: a fresh team sees no share reminder until teamai recall enable. main was no better there — it nudged towards a skill that skipRecall had not deployed — but it was quieter about it. The served text that described when the prompt appears was wrong in both directions and now matches.
  • npm pack shipped Python bytecode. Running the wiki scripts, which the e2e suite does, leaves __pycache__ under skill-data/, and files swept it in. Excluded in package.json, asserted absent in the tarball test, and the e2e run sets PYTHONDONTWRITEBYTECODE.
  • share's description still offered to publish reusable skills, the flow its own body sends to core. Sentence dropped, so the catalog no longer steers "publish a skill" at a recall-gated entry.
  • skill show <unknown> before init printed a stack trace. Not-found line, exit 1, covered in the e2e file.
  • The stub pre-approved every teamai command, including push, uninstall and recall enable, from the always-loaded unit; [feat] Serve builtin skill content from the CLI: deploy a discovery stub, add teamai skill get #678 asked for the serve commands only. Narrowed to Bash(teamai skill:*). The served skills grant nothing (see the round on 18c26b8): everything else a workflow runs goes through the agent's own permission prompt, as on main.
  • Six routing lines loaded core --full (36 KB) to reach one 5.7 KB reference. They name the file under $(teamai skill path core)/references/ instead.
  • uninstall misreported a failed delete as "holds files TeamAI did not put there; the packaged files were removed", both false, with the error never printed. It now names the file and the error; a test makes the stub directory read-only and asserts the sentence.
  • The stub directory archived under a different layout (<tool>/.claude-skills-teamai/teamai/) than the legacy trees (<tool>/.claude-skills/<skill>/). One layout now, as the design doc says.
  • Housekeeping the review caught: no CHANGELOG.md entry (added); a dead isRecallEnabled import in init.ts; the wiki skill still introducing itself as team-wiki-codebase in its heading and overview; a JSDoc block sitting on the wrong declaration; stale byte counts in the design doc and the served wiki text; the two usage guides silent on the recall refusal and on the archive under ~/.teamai/removed-skills/, and one paragraph that had drifted between the languages.
  • Recorded rather than changed: [feat] Serve builtin skill content from the CLI: deploy a discovery stub, add teamai skill get #678 sketched skill get --json; it is not implemented, the content is markdown for an agent and the machine-readable half is skill list --json. The design doc says so now. The legacy names classifying as [builtin] by name alone, even for a directory pull kept because it holds a member's file, is the consequence of pathname ownership the design doc already accepts, and is written next to it with the retirement of LEGACY_BUILTIN_SKILL_NAMES as the resolution. The recall gate fails open when the team config cannot be loaded, not only when there is none; the doc says that too.

Round on e4aa6ca, and the pre-push review that followed

The reviewer's one finding on e4aa6ca: Chinese in the stub's description and in setup's references, flagged a third time. Removed, as the language section now says, and guarded by a test. The other three findings from that comment were already fixed on e4aa6ca.

Then two review passes over the whole branch, one on src/, one on content and docs, before pushing. What they found, and what was done:

  • uninstall deleted through a linked ~/.claude/skills. It called the prune without the base directory, so only the leaf was checked; with the skills root linked at a dotfiles checkout, the packaged files inside that checkout went. removeOwnedFiles now owns the check, so pull, deploy and uninstall apply the same one. Test: a linked skills root survives uninstall --force, red on e4aa6ca.
  • A linked ~/.claude blocked the stub and the migration. Narrowing the guard to the last two components for that reason was reverted in the next round (below): it let a link at ~/.config/opencode through.
  • share was served to read-only HTTP teams, where its last step, teamai contribute, always refuses; reportingOnly used to skip it at deploy time. The gate carries a reason (recall | read-only) with its own message, and skill list --json reports blockedBy instead of blockedByRecall. The field is new in this PR, so nothing on main reads the old name.
  • The bytecode rule claimed any file under any __pycache__, in any CLI-owned directory. It now claims only the .pyc of a shipped script. Test, red on e4aa6ca.
  • recall disable pruned for an uninstalled Codex, reaching the shared .agents/skills; it has deployment's install gate now. The source-team guard lost the legacy names when BUILTIN_SKILL_NAMES narrowed, so a source removal could delete a legacy tree wholesale; restored.
  • Routing: the admin wrap-up and the stub still sent "share what I learned" to share with no word about recall, and the stub filed "share this with my team", the publish-a-skill phrase, under share. Both fixed; the stub says publishing lives in core. teamai recall enable was described as turning recall on "for the team"; it writes this member's override, and the text now names sharing.recall.enabled beside it. contribute-member.md still called the learning flow automatic and tool-dependent.
  • {SKILL_DIR}: serving resolves the sentence that defines it too, so a reference file opened on its own showed a placeholder nothing defined. The definition now says how such a file spells it.
  • Docs: uninstall removes the archive along with ~/.teamai/ (now said, in both guides); the zh guide said a directory with a member's file is kept whole; the product overview lacked the recall gate on the hint; the design doc's release, tag-count and byte figures were stale; the CHANGELOG now notes the language of generated documents (since restored to Simplified Chinese, round on 238e205), and the wiki skill says to keep an existing knowledge base's file names.
  • Smaller: skill show resolved the packaged skill before it knew it needed to; its card label was a column off and said a served skill was "not installed in any agent yet"; a comment still named bare /teamai; skill get --all carried a branch its own comment called impossible; two tests set TEAMAI_COMMAND_TABLE_ONLY without restoring it; a new two-scope test covers the inheritUserScope backup path, which had none.
  • Recorded rather than changed: a team-repo skill that reuses a legacy built-in name (team-wiki-codebase) is synced and then pruned. Push refuses those names, and on main the built-in overwrote such a skill on every pull, so the name was already unusable; filtering it out of team sync is a behaviour change of its own. Prune warnings on the session-start hook path go to stderr, which hosts hide for a successful hook, and log.warn is not written to debug.log; that is the logger's behaviour for every warning, not this PR's to change.

Real-CLI rows on df6f018 (superseded by the b4f675f rows below):

Claude, Codex, CodeBuddy, OpenCode   pull over a seeded previous stub (3 lines of Chinese)
                                     teamai/SKILL.md IDENTICAL to packaged in all four, 0 CJK lines
                                     skill get --all --full: 208 977 bytes, 0 CJK lines
~/.claude/skills -> dotfiles         uninstall --force: dotfiles teamai/SKILL.md intact, one warn line
kind: http, recall on                skill get share exit 1 "read-only HTTP source"; --all skips it;
                                     list --json share blockedBy "read-only", path null; get core exit 0
git, recall off (default)            skill get share exit 1 "share needs recall"

The github and gitlab rows above were run on earlier heads. This round changes no provider code and nothing that runs before the repo is on disk, which is where those rows differ from git.

Round on df6f018

The reviewer's two findings on df6f018:

  • The link guard checked only the skill directory and its parent — right, and a regression that round introduced. A link at ~/.config or ~/.config/opencode sits above both, so the prune walked, deleted and wrote through it. The full walk from the tool's base directory is back, and removeOwnedFiles now requires the base, so uninstall runs the same walk: each skill directory in its plan carries the base its skills root hangs off (the OpenClaw workspace root is its own base). The price is the one the previous round tried to avoid: a member whose whole ~/.claude is a link keeps the pre-stub trees and gets a warning naming the path. Deleting through a link is the thing the prune must never do, so that member is told rather than guessed for. Two tests, both red on df6f018: a linked ~/.config/opencode stops deploy and prune, and a linked ~/.claude stops uninstall.
  • No test record for the reviewed head — the review read the description before it was re-pointed; the push went out a minute before the edit. The record below is for b4f675f, and this time the description was updated before the push.

Real-CLI rows on b4f675f (re-run on fe0540e, same results, plus the skill show row in the next round):

Claude, Codex, CodeBuddy, OpenCode   pull over a seeded previous stub (3 lines of Chinese)
                                     teamai/SKILL.md IDENTICAL to packaged in all four, 0 CJK lines
                                     skill get --all --full: 208 977 bytes, 0 CJK lines
~/.claude -> dotfiles/claude         pull exit 0: old stub, legacy wiki tree and references intact
                                     in the checkout, 2 warnings naming the link
~/.config/opencode -> dotfiles       pull exit 0: wiki tree intact, no stub written through the link
~/.claude/skills -> dotfiles         uninstall --force: dotfiles teamai/SKILL.md intact, one warn line
kind: http, recall on                skill get share exit 1 "read-only HTTP source"; --all skips it;
                                     list --json share blockedBy "read-only", path null; get core exit 0
git, recall off (default)            skill get share exit 1 "share needs recall"

Round on b4f675f

The reviewer's two findings on b4f675f, both fixed in fe0540e:

  • The share reminder was not gated on a read-only HTTP source. In practice the dispatcher never runs it there: contributeCheckHandler is registered gitOnly (src/hook-handlers.ts:635) and filterHandlersForConfig drops those for repo.kind === 'http'. But the gate claimed to match share's and did not say so itself, so contributeHintAllowed now checks the source too. Test: an HTTP team with recall on gets no hint, red on b4f675f.
  • skill show teamai-share-learnings exposed a stale legacy tree. locateSkill searched agent directories before the package, so a directory a pull had not pruned yet was shown with its path while the gate refused share. A legacy built-in name now skips the agent search and goes to the packaged skill and its gate; share, codebase and the other ordinary aliases keep a member's own directory first. Test, red on b4f675f.

Real-CLI rows on fe0540e: every row of the previous round, same results, plus:

stale .claude/skills/teamai-share-learnings, recall off
                                     skill show teamai-share-learnings exit 1 "share needs recall",
                                     the stale path is not printed

Round on fe0540e

The reviewer's one finding on fe0540e, fixed in 18c26b8:

  • Built-ins were deployed and pruned under the tool root, not where the tool keeps skills. deployBuiltinSkills joined the base directory with the configured path, while team-skill sync resolves it through skillsDirForTool: OpenClaw's workspace, and HERMES_HOME for Hermes. main had the same join, but there the built-ins were extras; here the stub is the only unit, so those agents got it in a directory they never read, and their legacy trees were pruned from the wrong place. Deploy, the legacy prune, recall disable and uninstall now resolve the same directory. The link guard starts at the tool's base directory when the skills directory sits under it, else at that directory's parent (a HERMES_HOME outside the home directory). Two tests, both red on fe0540e: Hermes under HERMES_HOME and OpenClaw's workspace receive the stub and the prune, and uninstall removes the Hermes stub there.

Real-CLI rows on 18c26b8: every row of the previous rounds, same results, plus:

Hermes, HERMES_HOME outside HOME     pull exit 0: stub IDENTICAL under $HERMES_HOME/skills, legacy tree
                                     pruned there, nothing written under ~/.hermes;
                                     uninstall --force removes the stub from $HERMES_HOME

Round on 18c26b8

The reviewer's three findings on 18c26b8, all fixed in e747a75:

  • An external skills root was not itself checked for a link. With HERMES_HOME or an OpenClaw workspace outside the tool's base, the guard started at that root, so a linked HERMES_HOME was walked through. The guard starts one level above such a root now. Test: a linked HERMES_HOME outside the home directory gets no stub and loses no file, red on 18c26b8.
  • A config migration could corrupt skill get and skill list --json. The share gate loads the config, and loading an upgraded one runs migrateLegacyRoleConfig, which reports with log.info on stdout. That line landed ahead of the skill content and the JSON. Config loading reports on stderr for that call; setStderrOnly now returns the previous mode so the gate restores it. Test: a migrating config leaves skill get share starting at its frontmatter and skill list --json parseable, with the line on stderr, red on 18c26b8.
  • allowed-tools in the served skills granted nothing (P2, fixed because the round had blockers). skill get prints that frontmatter as command output; the agent never reads it as skill metadata, so the earlier claim that it removed permission prompts was wrong, and this description no longer makes it. The four lines are gone and the content test fails if one comes back. The stub's Bash(teamai skill:*) is the only grant; widening it to Bash(teamai:*) would undo the [feat] Serve builtin skill content from the CLI: deploy a discovery stub, add teamai skill get #678 narrowing, so teamai pull and the rest prompt through the agent's own flow, as they did on main.

Real-CLI rows on e747a75: every row of the previous rounds, same results, plus:

HERMES_HOME -> dotfiles, outside HOME  pull exit 0: target wiki tree intact, no stub written through the link,
                                       2 warnings naming it
legacy config (no primaryRole),        skill list --json exit 0, stdout parses as JSON;
roles manifest with `hai`, recall on   stderr "Migrated legacy teamai config to default role profile: hai";
                                       config.yaml migrated to primaryRole: hai

Round on e747a75

The reviewer's one finding on e747a75, fixed in 238e205:

  • A linked COPILOT_HOME was not checked. The guard started at the tool's base directory, which for Copilot in user scope is COPILOT_HOME itself, so the walk began below the link, and pull and uninstall wrote and pruned through it. The guard now starts at the scope root (home, or the project root), where a link at or above is ordinary, in deploy, the legacy prune and uninstall alike; a root configured outside the scope root (HERMES_HOME, an OpenClaw workspace) still has the walk start just above it. Two tests, both red on e747a75: a linked ~/.copilot stops deploy and prune, and stops uninstall.

Real-CLI rows on 238e205: every row of the previous rounds, same results, plus:

~/.copilot -> dotfiles (COPILOT_HOME)  pull exit 0: target wiki tree intact, stub not written through the link;
                                       uninstall --force: target stub intact

Round on 238e205

The reviewer's three findings on 238e205, all fixed in f16e725:

  • Generated documents changed language (P1). share and wiki had moved learnings and knowledge-base documents from always Chinese to the session language. Serving the instructions does not need that, so both say "Simplified Chinese" again, in an English instruction, and the CHANGELOG entry says the output language is unchanged. The language section above is rewritten to match.
  • Every init failure read as "not initialized" (P2). skill show and skill list caught anything autoDetectInit threw and pointed at teamai init, so a missing teamai.yaml or an unreadable config sent the member to re-init over a working setup. requireInit throws a tagged NotInitializedError now (same message); only that falls back to the packaged catalog, and anything else propagates the way it does for teamai status. Test: a broken config rejects instead of printing the fallback, red on 238e205.
  • $(teamai skill path …)/… was unquoted (P2), word-split in a shell command exactly like an unquoted {SKILL_DIR}. All ten occurrences are double-quoted, and the quoting test covers the form (red against the previous setup/SKILL.md).

Real-CLI rows on f16e725: every row of the previous rounds, same results, plus:

recall on                   skill get share: "Write the document in Simplified Chinese"; skill get wiki:
                            "Write the knowledge-base documents in Simplified Chinese"
                            skill get --all --full: 0 unquoted `$(teamai skill path`
teamai.yaml missing         skill list exit 1 with "Team config (teamai.yaml) not found", no
                            "Not initialized" line (same failure `teamai status` reports)
fresh HOME                  skill list exit 0, catalog printed, "Not initialized: run `teamai init` …"

Round on f16e725

The reviewer's one finding on f16e725, fixed in 427f7e2:

  • A malformed local config was still "not initialized". loadLocalConfig() returns null both for a missing file and for one that fails to parse, validate or migrate (it logs the reason), and requireInit() turned every null into NotInitializedError, so the previous round's narrowed catches still fell back to the packaged catalog on a broken config. Only an absent file is NotInitializedError now; an existing file that could not be used is an error naming its path, in requireInit and the user branch of requireInitForScope. Covered through the real loader (config-not-initialized.test.ts: a missing file, an unparseable one and an invalid one; the last two red on f16e725) and through the built binary in the e2e file (skill list and skill show core on an unparseable config exit non-zero with "could not be read" and no "not initialized" line).
  • Not changed: a broken project config makes detectProjectConfig return null, and detection then falls back to the user config. That is how every command on main resolves scope; changing detection is its own change.

Real-CLI rows on 427f7e2: every row of the previous rounds, same results, plus:

~/.teamai/config.yaml does not parse   skill list exit 1, skill show core exit 1, both
                                       "The teamai config at … could not be read", no "not initialized" claim

Round on 427f7e2

The reviewer's three findings on 427f7e2. Two are fixed in 99c0722; the third is recorded, not done.

  • Cleanup deleted by pathname alone — fixed. A file at a path a release packaged was removed whatever it held, so a member's edit, a skill of their own under a legacy name, or a root TeamAI never managed lost its files. Ownership is now path and content: PACKAGED_SKILL_DIGESTS (src/packaged-skill-digests.ts, generated from every tag and main before the stub) holds 37 shipped versions across 21 paths, a skill-root SKILL.md compared by its body because releases before 0.17 shipped no frontmatter and the deploy of the day repaired it on disk. The current stub counts as ours by the packaged copy. An earlier round rejected content checking for fear of CRLF; that does not hold, because these files were copied from the npm tarball byte for byte, never checked out by git, so a mismatch is the member's change and keeping it is the safe direction. Unit tests mock the table with a stand-in for shipped content (a test keeps the stand-in on the real table's paths); three new tests are red on 427f7e2, and existing ones that expected an edited file to be pruned now expect it kept.
  • A stale second Codex copy survived — fixed. The stub goes to .agents/skills/teamai when a copy lives there, and the copy an earlier release left in .codex/skills/teamai kept its old SKILL.md and references. retireOtherCodexCopy removes it by the same ownership rule, archived first, and names it when a member's file keeps it. Two tests, red on 427f7e2.
  • No real teamai init against GitLab — done afterwards: We ran teamai init against GitLab with a GitLab Personal Access Token, and it passed. (It needs a PAT with api scope: the provider sends GITLAB_TOKEN as PRIVATE-TOKEN, which a glab OAuth login does not satisfy.)

Real-CLI rows on 99c0722, with the real v0.25.0 trees taken from the tag (git archive v0.25.0 skills), so the digest table is what is exercised, not the test stand-in. Built CLI, isolated HOME, local bare remote (provider: git), Claude, CodeBuddy and Codex:

claude      unedited 0.25.0 wiki SKILL.md, scan_repo.py, share tree, teamai/references: removed, archived
            phase0-collection.md edited in place: kept;  member's my-notes.md: kept;  wiki dir kept and named
            stub IDENTICAL to packaged
codebuddy   member's own skill named teamai-share-learnings: kept and named;  unedited wiki tree: removed
codex       0.25.0 teamai tree in both roots: stub IDENTICAL in .agents/skills, references removed,
            the .codex/skills/teamai copy retired
archive     53 files under ~/.teamai/removed-skills
uninstall   member's my-notes.md and own teamai-share-learnings kept; stubs removed

Every row of the previous rounds re-ran on 99c0722 with the same results; the Hermes row now seeds the real 0.25.0 SKILL.md, since arbitrary text at that path is now, correctly, the member's.

Round on the review of 04:06 (all findings, after #739)

#739 made the reviewer report every finding; its first full pass ran on 427f7e2, before 99c0722 was pushed. Its findings and what happened to each:

Finding Outcome
P1 GitLab teamai init not run Run with a GitLab PAT: passed.
P1 cleanup trusts the pathname Fixed in the content-ownership commit (digest table, real v0.25.0 trees verified).
P1 legacy pruned before the stub is installed Fixed in 327f9cd: the prune runs only after the stub deployed for that agent; a refused stub keeps the old trees and says so. Test red before.
P1 Codex keeps a stale second copy Fixed with retireOtherCodexCopy.
P1 share gate fails open on any config error Fixed in 327f9cd: only a machine with no config is served; an unloadable one blocks with blockedBy: "config". Test red before.
P1 KB template names code-to-knowledge --update Fixed: teamai codebase --extract <repo> --project <slug> --incremental.
P2 reference lists hidden commands Fixed: rendered from Help.visibleCommands, so it matches --help. hook-dispatch stays because it is not hidden.
P2 removeEmptyDirs swallows errors Fixed: only "still holds something" is expected; anything else is reported. On macOS rmdir of a non-empty directory under a read-only parent is EACCES, so the contents decide, not the code. Test red before.

Real-CLI rows on 327f9cd: every earlier row and the real v0.25.0 row, same results, plus:

config.yaml does not parse   skill get share exit 1 "the teamai config on this machine could not be loaded",
                             stdout empty; skill get core exit 0
commands.md                  33 command sections, none of track / contribute-check / todowrite-hint /
                             mr-hint / dashboard-report

Round on 327f9cd

Finding Outcome
P1 GitLab teamai init not run We ran teamai init against GitLab with a GitLab Personal Access Token, and it passed.
P1 SKILL.md ownership ignores frontmatter Fixed in 50e2b92: digests are whole files. Every release from 0.16.1, the first whose deploy repaired frontmatter, shipped complete frontmatter, so what is on disk is what was shipped. Real v0.25.0 SKILL.md with only its description edited: kept. Test red before.
P1 references pruned before the stub is copied Fixed: the stub is written first, then the references it no longer points at are pruned. Test red before.
P1 Codex resolution mutates before the link guard Fixed: resolved side-effect free; the other copy is handled under the guard. Test with a linked .codex/skills red before.
P1 broken project config bypasses the gate Fixed: findUnreadableProjectConfig reports it through an optional sink on detection (no other caller changes behaviour), and share blocks with blockedBy: "config". Tests through the real loader.
P2 reminder fails open on config errors Fixed: only a machine with no config at all gets it.
P2 bytecode owned by filename Fixed: ours only beside a script proven ours by content; a link is never ours.
P2 hook-dispatch in the reference Fixed: hidden (hook plumbing); still runs as before.
P2 "every command" claim Fixed: the reference and setup say every public command and every flag.
P2 init says ready when nothing deployed Fixed: announced only when the stub landed; otherwise a warning with the way out.
P2 failed Codex backup reported as foreign files Fixed: a failed backup or delete is named as such.
P2 design doc on teamai-workflow / teamai-import Fixed: those are never touched.

Real-CLI rows on 50e2b92: every earlier row and the real v0.25.0 row, same results, plus a real v0.25.0 wiki SKILL.md with only its description edited: kept and named.

Round on 50e2b92

The reviewer's one finding, fixed in 78d2b2b: findUnreadableProjectConfig dropped a recorded error whenever detection went on to find a later candidate, so a broken partition config followed by a valid legacy .teamai/ config returned null and the share gate answered with the fallback's team; and an existing config file that was empty or unreadable returned without reaching the sink. It now reports the first unreadable file whether or not a fallback loads, and an empty or unreadable file is reported too. Two tests through the real loader and a real git repo (broken partition plus valid legacy config; empty config), both red on 50e2b92. Real CLI: an empty .teamai/config.yaml makes skill get share exit 1 with "the teamai config on this machine could not be loaded" and an empty stdout.

What main changed underneath the skills

The four commits between the previous base and cd3e0e6 were checked against every file under skill-data/ and the stub, because a skill that documents behaviour main just changed would ship stale on the day it lands.

Commit What it changes Effect on the served skills
#700 project-scoped hooks, MCP and env projects: beside roles: on hooks.yaml, mcp.yaml, env.yaml; Synced 1 of 3 env variable(s); doctor filter None. manage-admin.md lists the teamai mcp and teamai env commands and says the definitions travel with the team repo; it never described the YAML entries or their per-entry roles:, so there is no half-sentence to complete. No Commander option or command was added, and commands-reference.test.ts confirms commands.md still matches the table.
#695 Qoder CN new built-in target qoder-cn, user root ~/.qoder-cn None in content: no skill enumerates agents (the only agent names in skill-data are the --agent examples that come from Commander's own help text). In behaviour, the stub deploys and the prune runs there like everywhere else, per the row above.
#720 Stop-hook nudge relay drops the verbatim wrapper from buildHint, relays through relayWhenHidden The one conflict, resolved as described above. manage-admin.md describes the nudge's kill switch and resolution order, which #720 did not change.
#729 CI flakiness test-only None.

gitlab row — now run, against a real private gitlab.com project (provider: gitlab in teamai.yaml, built CLI, isolated HOME, Claude and Codex installed, the pre-stub trees and a member's own file seeded). The project was created for the run and deleted after it:

pull                     my-skill synced; team-wiki-codebase kept and named (holds the member's
                         references/methodology/my-notes.md); teamai/SKILL.md deployed to
                         .claude/skills and .codex/skills, IDENTICAL to packaged
skill path                (no name)   exit 1, "missing required argument 'name'"
skill path share          exit 1      "share needs recall, which is disabled for this team"
skill path wiki           exit 0      prints skill-data/wiki
recall status             disabled    team sharing.recall.enabled false, no user override
skill get share           exit 1      | skill get --all  stderr "Skipped share"
skill show share          exit 1
push --all --dry-run      offers teamai-workflow only (the member's); no CLI-owned tree
recall disable            teamai-share-learnings removed; team-wiki-codebase, my-skill, teamai kept
uninstall --force         6 skill dirs removed, including .agents/skills/teamai;
                          .agents/skills/my-shared-skill and .claude/skills/teamai-workflow remain

One detour worth recording, because it is easy to read as a gap in this row. glab's stored gitlab.com credential is an OAuth token: GET /api/v4/user is 401 with a PRIVATE-TOKEN header and 200 with Authorization: Bearer, and src/providers/gitlab/gitlab-api.ts only ever sends PRIVATE-TOKEN. That affects teamai init against GitLab and nothing else — it is the one command in the flow that calls the GitLab API to resolve and clone the repo. It does not affect the row above: everything this PR changes runs after the repo is on disk (pull, the prune, skill get/path/show/list, push, recall, uninstall read ~/.teamai/config.yaml and the local clone, and touch no GitLab API). The team repo here is a real private gitlab.com project with provider: gitlab, reached over the network by git exactly as init would have left it, so the row is complete for what this PR changes.

The init call itself against GitLab needs a Personal Access Token with api scope rather than a glab OAuth login. We ran teamai init against GitLab with a GitLab Personal Access Token, and it passed. Two follow-ups, neither of them this PR's: a member who authenticated glab interactively hits an error that says "Set the GITLAB_TOKEN environment variable" without telling them their existing login is the wrong kind of token (worth an issue — accept a Bearer token, or detect the glab login and say so); widening this PR into GitLab auth would repeat the mistake the prune set was blocked for.

Found while running it, not part of this PR: teamai init has no non-interactive mode. With no terminal it starts the GitHub device-code flow and waits for the browser until the deadline expires, about five minutes, then exits 1. In CI or a headless agent that is a hung job with a log that does not say which credential is missing. Worth its own issue: fail fast when stdin is not a TTY, and say which token to set.

Merge Danger

Door: two-way, including what it deletes.

Reverting restores the packaged trees and the next pull redeploys them, so the code is reversible. The prune removes, from every installed agent on the member's machine, the files earlier releases packaged under team-wiki-codebase, teamai-share-learnings and teamai/references/. Only at the content a release shipped there: see the next paragraph. Leaving the unedited ones behind would keep the token cost this change exists to remove.

Ownership is proven by path and content. PACKAGED_SKILL_DIGESTS records the sha256 of every blob a release shipped under those trees (all 100 tags through v0.25.0 and main before the stub: 42 versions across 21 paths, whole files, frontmatter included; the deploy repaired frontmatter from 0.16.1 on, but every SKILL.md those releases shipped was already complete). The copies came from the npm tarball byte for byte, so an unedited one matches; an edited file, a member's own skill under a legacy name, or a root TeamAI never managed because toolPaths or HERMES_HOME moved does not, and stays with its directory. Every pruned file is still copied to ~/.teamai/removed-skills/<run>/<base>/<tool>/<skill-root>/<skill>/ — the machine's home, never the tool's base directory, which under project scope is the repo root — before it is removed — outside every agent directory, so nothing reads it back as a skill. A file whose copy fails is kept rather than removed, and named at warn level: a backup that did not happen must not authorise the delete. The path carries the run, the base directory and the skill root because none is unique on its own — two pulls land on the same day, inheritUserScope deploys the user base and then the project base in one process, and Codex prunes the same skill name from both .codex/skills and the shared .agents/skills. 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 — COPILOT_HOME included — is refused outright — neither pruned nor written through, link and target untouched — by pull, deploy and uninstall alike. Checking the skill directory alone would miss the common shapes, ~/.claude/skills or ~/.config/opencode linked at a dotfiles checkout, where every directory under the link is real. 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. A file that is archived but cannot then be deleted is reported rather than left to a debug line.

Nothing else is deleted. The digest table excludes teamai-wiki (0.13.0, 0.16.x — it predates the trees this migration is about, and widening a destructive set is its own change); it covers seven teamai/references/*.md, provider-tgit.md first shipped in 0.25.0. Every file removed is provably one the CLI wrote, by content; a file a member added beside them is kept, its directory with it, and pull names that directory and says why. Excluded agents are not touched at all, as the enabledAgents whitelist documents, and only Codex's own pass reaches Codex's shared .agents/skills.

Blast Radius: every agent directory on every member's machine.

The routing rests on one description. With a single deployed skill, the stub's description is the only text an agent sees at selection time, so it carries the triggers the wiki and share skills used to own, in English. If an agent triggers the skill and skips the command it names, it holds a 2.5 KB stub and nothing else; allowed-tools pre-approves only the teamai skill … commands, so the one command it asks for runs without a prompt and nothing else is pre-approved from the always-loaded unit.

Two more worth naming. Bash has to be available: an agent that cannot run a command reads nothing, and teamai skill path gives it a directory instead. And the content stops being greppable inside the agent directory, which teamai skill get --all and skill path cover.

@jeff-r2026 jeff-r2026 self-assigned this Sep 22, 2026
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/builtin-skills.ts:45teamai-workflow and teamai-import were never packaged/deployed in the base revision; they were only reserved names. Adding them to LEGACY_BUILTIN_SKILL_NAMES recursively deletes unrelated user-created skills with those names on every pull and uninstall. Only prune directories the CLI actually owned, or verify provenance before deletion.
  • [P1 blocking] src/skill-content.ts:264--all bypasses the recall gate and prints the complete share workflow when recall is disabled. skillPath() similarly exposes its directory at src/skill-content.ts:318. This defeats the runtime gate replacing the old deployment restriction; blocked skills must be filtered or rejected through every content-access path.
  • [P1 blocking] PR description — The required provider E2E matrix is incomplete: it explicitly says the gitlab and generic git rows were not run. The repository instructions require real-CLI verification for git, gitlab, and github; complete and document those runs before merge.
  • [P2 non-blocking] src/commands-reference.ts:42 — The generated “ground truth” drops Commander’s variadic marker, documenting <names...> and <skills...> as singular <names>/<skills>. Because the snapshot is generated by the same faulty renderer, tests preserve the error. Include Argument.variadic when formatting usage.

@SaulMoro

Copy link
Copy Markdown
Contributor Author

Addressed in dc45189 (all four points), PR description updated.

P1 — teamai-workflow / teamai-import in the prune set. Confirmed: neither was ever under skills/ in any tag (checked v0.24.0-beta.4 through beta.11 and origin/main); they only sat in the old BUILTIN_SKILL_NAMES guard. Both dropped from LEGACY_BUILTIN_SKILL_NAMES, so pull and uninstall now remove only team-wiki-codebase and teamai-share-learnings, the two directories the CLI actually wrote. Test added in skip-uninstalled-tools.test.ts: user-created skills with those two names survive a pull with the legacy trees beside them. Design doc corrected.

P1 — recall gate bypass. One refuseBlockedByRecall now backs every content path:

  • skill get --all leaves a blocked skill out and names it on stderr (⚠ Skipped share: needs recall …), exit 0 for the rest.
  • skill path <name> refuses with the same message as skill get <name>, exit 1.
  • skill list --json reports blockedByRecall: true and path: null for a blocked skill; skill list reads the flag from the catalog instead of re-checking.

Tests in skill-recall-gate.test.ts cover all three, on and off. While running this through the built CLI I found that --all was unreachable from the shell: get <names...> made Commander fail with missing required argument 'names' before the action ran. The positional is now [names...]; the "no name and no --all" case still exits 1 with the usage hint, and the skill-serving e2e now drives --all and the empty call through dist/index.js.

P2 — variadic marker. renderCommand appends ... when Argument.variadic is set. Snapshot regenerated: ten usages changed (skill get [names...], skill exclude add <skills...>, tags subscribe <tags...>, remove <type> <names...>, …). skill-commands-exist still passes against the regenerated table.

P1 — provider matrix. Generic git row now run with the built CLI in an isolated HOME against a local bare remote (provider: git, the layout the repo's own e2e tests use), with Claude, Codex, CodeBuddy and OpenCode present; full trace in the PR description. Summary:

pull                         one teamai/SKILL.md per agent, legacy trees gone, user skills teamai-workflow/teamai-import untouched
recall off                   skill get share exit 1 | --all serves core,setup,wiki + "Skipped share" | skill path share exit 1 | list --json share path null
recall enable                --all serves 4 | skill path share prints the directory
skill get core               byte-identical bar {SKILL_DIR}
uninstall --force            teamai gone from every agent, the two user skills remain

The gitlab row is still not run: this machine has no GITLAB_TOKEN, so neither the live GitLab test nor this matrix can reach a GitLab instance from here. The provider only affects clone and auth, which this PR does not touch; everything this PR changes is covered by the git run above and the earlier github run. If a maintainer with a GitLab token can run the pull/prune/uninstall rows, that closes the matrix; happy to do it if one is provided.

Runs on dc45189: npx tsc --noEmit clean; npx vitest run 3761 passed, 1 skipped, 3 failed in shell-profile.test.ts — that file is byte-identical to origin/main and fails there too on macOS (from #693), unrelated to this branch; npm run build ok; skill-serving e2e 5 passed.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] skill-data/wiki/SKILL.md:4 and skill-data/share/SKILL.md:6 introduce Chinese text that teamai skill get prints directly. This violates the trusted base rule that CLI user-facing output must be English; modifying AGENTS.md in this PR to add an exception cannot change the review criteria.
  • [P1 blocking] src/builtin-skills.ts:163 deletes every entry beside SKILL.md. Previous deployment overwrote known packaged files but did not delete user-added files, so custom references, metadata, or scripts that previously survived pulls are now irreversibly removed. Delete only known legacy packaged paths.
  • [P1 blocking] The PR description includes substantial real-CLI evidence, but explicitly says the required gitlab and generic git provider rows were not run. The trusted pre-PR test matrix requires actual verification for git, gitlab, and github.
  • [P2 non-blocking] src/builtin-skills.ts:64 checks the Codex shared .agents/skills directory while processing every enabled tool. If Codex is excluded but another tool is enabled, that other tool’s pass still deletes Codex’s legacy directories, violating the enabledAgents guarantee.
  • [P2 non-blocking] src/builtin-skills.ts:38 removes legacy built-in names from the set excluded during push. Immediately after upgrading, a user who runs teamai push --all before the next pull can publish the old team-wiki-codebase and teamai-share-learnings deployments into the team repository.
  • [P2 non-blocking] src/skill-cmd.ts:138 resolves packaged aliases before installed-agent skills. Common custom names such as codebase, default, or learning are therefore shadowed by built-ins in skill show; additionally, canonical served skills such as wiki are reported as [local-only] because only teamai is present in BUILTIN_SKILL_NAMES.

@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 41a71d9.

  • Recall gate through the real binary. The skill-serving e2e now has a HOME carrying a team whose recall is off (the default for a fresh team). Through dist/index.js: skill get share exit 1, skill get --all serves three and reports Skipped share on stderr, skill path share exit 1, skill list --json reports share with blockedByRecall: true, path: null; after recall enable all four paths serve it. The earlier e2e HOME has no team config, so the gate failed open there and neither the --all bypass nor the required positional would have been caught.
  • Stub description budget. The one description every agent sees is 821 characters; a test now fails past 1024.
  • Content audit against origin/main. Every one of the 20 old skills/ blobs has a counterpart under skill-data/ (12 byte-identical, 8 differ only in path or name rewrites); every {SKILL_DIR} reference resolves; --full serves no scripts, .pyc or binaries. Fixed: {SKILL_DIR} is now defined in core/setup/share next to their reference tables (a reference read from disk carries the literal), the wiki overview draws the served layout instead of the old team-wiki-codebase/ tree, team-wiki-codebase stays as a trigger word in the stub description, and the --full sizes are the measured ones. skill-commands-exist also scans the stub now.
  • Docs. The design doc and both usage guides lead with the point of the change, content versioned with the installed CLI, and keep the size figures as the secondary cost.

Runs: npx tsc --noEmit clean; npx vitest run 3762 passed, 1 skipped, the same 3 pre-existing shell-profile failures as on origin/main; npm run build ok; skill-serving e2e 6 passed.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/skill-cmd.ts:138 bypasses the recall gate. skill show share resolves the packaged skill and src/skill-cmd.ts:203 prints its directory even when recall is disabled, allowing callers to read the content that skill get and skill path intentionally withhold. Apply blockedByRecall() before returning or printing packaged share.
  • [P1 blocking] src/builtin-skills.ts:64 checks .agents/skills for every enabled tool, not only Codex. If Codex is excluded or uninstalled but another agent is enabled, that other agent’s deployment still deletes Codex’s legacy shared skills, violating the whitelist guarantee and performing an irreversible deletion. Only include SHARED_AGENT_SKILLS_PATH when tool === 'codex'.
  • [P1 blocking] The required provider test matrix is incomplete. The trusted AGENTS.md requires real-CLI verification for git, gitlab, and github, but the PR explicitly says the GitLab row was not run. Complete and document that row before merge.
  • [P1 blocking] skill-data/share/SKILL.md:6 and subsequent lines contain Chinese text that is emitted verbatim by teamai skill get. This violates the checked-out base rule that CLI user-facing output must be English and production code must contain no Chinese. Modifying AGENTS.md in this PR cannot change the trusted review criteria.
  • [P2 non-blocking] src/skill-cmd.ts:138 resolves core, setup, wiki, and share as packaged built-ins, but BUILTIN_SKILL_NAMES at src/builtin-skills.ts:38 contains only teamai. Consequently, skill show core reports Source: [local-only] alongside a package path. Classify packaged results directly as built-in or include served skill names in classification.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
… from Codex

Review follow-up on Tencent#699.

- `skill show <served skill>` refuses a recall-blocked skill with the same
  message and exit code as `skill get` / `skill path`; it printed the
  directory those two withhold.
- A skill resolved from skill-data/ is classified `[builtin]` directly.
  BUILTIN_SKILL_NAMES only knows the deployed stub, so `skill show core`
  reported `[local-only]` beside a package path.
- pruneLegacyBuiltinSkills reaches `.agents/skills` only on Codex's own
  pass. Another enabled tool's pass deleted Codex's legacy copies while
  Codex was excluded, against the enabledAgents guarantee.
- The share skill and its references are written in English; the generated
  document still follows the session's language. The AGENTS.md exception
  for Chinese skill-data output is dropped.
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 174ab6a.

P1 — skill show bypassed the recall gate. skillShow now checks blockedByRecall for a skill resolved from the package and refuses with the same message and exit code as skill get / skill path, before any path is printed. Once recall is on, the card prints as before.

P1 — .agents/skills pruned from every tool's pass. pruneLegacyBuiltinSkills now includes SHARED_AGENT_SKILLS_PATH only when tool === CODEX_TOOL. Test added: Claude enabled, Codex installed but excluded, a legacy ~/.agents/skills/team-wiki-codebase survives the pull byte-for-byte (fails on the previous revision, verified by reverting the fix). Design doc corrected.

P1 — Chinese in skill-data/share. share/SKILL.md, references/doc-template.md and the one line in references/contribute-member.md are now in English; teamai skill get share --full prints 0 CJK lines. The behaviour is unchanged: the learning document is still written in the session's language. The AGENTS.md / CLAUDE.md exception added earlier is dropped. The wiki skill remains in Chinese: it is the team-wiki-codebase content that origin/main already ships and deploys into every agent (478 lines in its SKILL.md alone, ~2,000 across references and scripts), moved rather than introduced here. Translating it is its own change; happy to open an issue for it.

P2 — skill show core reported [local-only]. A skill resolved from skill-data/ is classified { kind: 'builtin' } directly instead of by name lookup in BUILTIN_SKILL_NAMES.

P1 — GitLab row. Unchanged from the previous round: this machine has no GITLAB_TOKEN, so neither the live GitLab test nor the matrix can reach a GitLab instance from here. Everything this PR changes sits after clone and auth and was exercised with git (below) and github (earlier run). A maintainer with a GitLab token running pull / prune / uninstall once closes the row.

Built CLI, isolated HOME, provider: git against a local bare remote, Claude and Codex installed, legacy trees seeded in .claude/skills and .agents/skills:

enabledAgents: [claude]   pull        .claude/skills → teamai only; .agents/skills/team-wiki-codebase kept (# codex copy)
enabledAgents: [claude, codex]  pull  .agents/skills legacy gone; .codex/skills/teamai deployed
recall off                skill show share   exit 1  "share needs recall, which is disabled for this team"
                          skill show core    exit 0  Source [builtin], Package path …/skill-data/core/
recall enable             skill show share   exit 0  Source [builtin], Package path …/skill-data/share/
skill get share --full    7 929 bytes, 2 references, 0 CJK lines
uninstall --force         teamai removed from .claude/skills and .codex/skills

Runs on 174ab6a: npx tsc --noEmit clean; npm run build ok; npx vitest run 3766 passed, 1 skipped, the same 3 pre-existing shell-profile Windows cases that fail on origin/main on macOS; skill-serving e2e 6 passed, now including skill show on both sides of the gate.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Legacy skills can be pushed back into the team repo after upgrading. BUILTIN_SKILL_NAMES now contains only teamai, so scanLocalForPush() no longer excludes existing team-wiki-codebase or teamai-share-learnings directories. If a user runs teamai push --all before their first post-upgrade pull, those obsolete bundled skills are treated as new user skills and published. Keep legacy names excluded from push until migration support is removed. src/builtin-skills.ts:38
  • [P1 blocking] Disabling recall no longer removes the previously deployed sharing skill. An upgrade followed directly by teamai recall disable leaves teamai-share-learnings installed, allowing the old workflow to remain discoverable despite recall being disabled. removeRecallArtifacts() must still remove that legacy directory. src/recall-toggle.ts:24
  • [P1 blocking] The new CLI command emits Chinese user-facing output. teamai skill get wiki prints this file verbatim, conflicting with the trusted rule that CLI user-facing output must be English. The built-in workflow content must be English or localized behind an explicit locale mechanism. skill-data/wiki/SKILL.md:4
  • [P1 blocking] The required GitLab end-to-end matrix was not run. The PR description explicitly says the gitlab row was skipped, while the trusted pre-PR requirements mandate real-CLI verification for git, gitlab, and github.
  • [P2 non-blocking] Plain teamai skill list cannot discover packaged skills before initialization. It calls status.list(), which immediately requires autoDetectInit(), before printing the built-in catalog. This conflicts with the package-level discovery behavior supported by skill get; only skill list --json currently works without team configuration. src/skill-cmd.ts:108

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
… push, wiki in English

Review follow-up on Tencent#699.

- resolveServableSkill is the only way to obtain a PackagedSkill outside
  skill-content.ts; it returns `blocked` instead of the skill, so `get`,
  `path`, `list` and `show` inherit the recall gate by construction.
- push never offers `team-wiki-codebase` / `teamai-share-learnings` as new
  user skills: between the upgrade and the first pull they are still on
  disk (isCliOwnedSkillName).
- `recall disable` removes the legacy `teamai-share-learnings` directory
  again (LEGACY_RECALL_SKILL_NAMES), skipping excluded agents.
- `skill list` prints the packaged catalog before `teamai init`, with a
  hint for the team half, instead of failing on the team listing.
- skill-data/wiki (SKILL.md, 14 references, 2 scripts) translated to
  English. Generated document names follow one glossary; validate_kb.py
  still recognises headings of knowledge bases built by the previous
  release, matched by code point so the source stays ASCII.
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 13716ef.

P1 — legacy trees pushed back into the team repo. scanLocalForPush now skips isCliOwnedSkillName(dir): the current stub and both legacy directories. Test in skills.test.ts: with team-wiki-codebase, teamai-share-learnings, teamai and a user's teamai-workflow on disk, only teamai-workflow is offered.

P1 — recall disable left the old share skill. removeRecallArtifacts removes teamai-share-learnings again (LEGACY_RECALL_SKILL_NAMES, through the same Codex-aware prune as pull, skipping excluded agents). Test in recall-toggle.test.ts: the legacy share directory goes, team-wiki-codebase, the stub and a user skill stay.

P1 — Chinese in the wiki skill. skill-data/wiki/ is now English: SKILL.md, all 14 references and both scripts. teamai skill get wiki --full prints 0 CJK characters. Generated document names follow one glossary across files ({project_name} Technical Architecture.md, XX_{component}_Design.md, G1_{project_name}_Component_Dependency_Matrix.md, …). validate_kb.py still recognises the headings of knowledge bases built by the previous release: its two heading regexes match both spellings, with the Chinese side written as \uXXXX escapes so the source itself carries no CJK. No teamai … command changed; skill-commands-exist and commands-reference pass on the translated files.

P2 — skill list before init. The human-readable listing probes autoDetectInit and, when there is no team, prints a one-line hint and the packaged catalog instead of throwing. --json is unchanged. Test in skill-list-uninitialized.test.ts.

Also in this commit. The recall gate now lives in one place: resolveServableSkill(name) returns { found, skill } | { blocked, reason: 'recall' } | { not-found } and is the only export that yields a PackagedSkill. skill get, skill path, skill list and skill show consume it, so a new command cannot receive a directory the gate withholds. The skill show bypass from the previous round would have been unrepresentable under this shape. skill path switches over the union with a never check.

P1 — GitLab row. Still not run. This machine has no valid GitLab credential (glab auth status returns 401, SSH host key not trusted), so neither the live test nor the matrix can reach a GitLab instance from here. Nothing in this PR touches clone or auth; the git and github rows cover every changed path. A maintainer with a GitLab token running pull / push --dry-run / recall disable / uninstall once closes it.

Built CLI, isolated HOME, provider: git against a local bare remote:

no ~/.teamai            skill list                 exit 0, hint "run teamai init", 4 catalog rows
                        skill get wiki --full      132 458 bytes, 14 references, 0 CJK
                        scan_repo.py / validate_kb.py --help   English usage text
legacy trees on disk    push --all --dry-run       offers my-skill only
before first pull       recall disable             teamai-share-learnings removed; team-wiki-codebase, my-skill kept
                        pull                       team-wiki-codebase removed, teamai/SKILL.md deployed, IDENTICAL to packaged
recall off              skill show share           exit 1, "share needs recall"
                        skill list                 share marked "(needs recall — teamai recall enable)"

Runs on 13716ef: npx tsc --noEmit clean; npm run build ok; npx vitest run 3769 passed, 1 skipped, the same 3 pre-existing shell-profile Windows cases that fail on origin/main on macOS; skill-serving e2e 6 passed.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Destructive cleanup deletes untracked user files without proving ownership. src/builtin-skills.ts:186 removes every entry except SKILL.md, while src/builtin-skills.ts:94 deletes entire legacy directories. Previous overwrite: true copies did not delete additional user-created references/scripts, so those files could survive prior pulls and are now irreversibly removed. Delete only known packaged paths or verify ownership/content before pruning.
  • [P1 blocking] Required GitLab end-to-end coverage is explicitly missing. The trusted AGENTS.md requires real-CLI verification for git, gitlab, and github, but the PR description says “Not run: the gitlab row.” Complete and document that matrix before merge. The automated e2e evidence also says “4 passed,” while the changed test file contains six tests, so it does not clearly cover the final revision.
  • [P2 non-blocking] Built-in aliases shadow local installed skills. src/skill-cmd.ts:174 resolves packaged skills before searching installed agents. Consequently, local-only skills named default, join, codebase, learning, learnings, or even share are reported as the aliased built-in—or blocked by recall—instead of their actual metadata. Preserve the existing local-skill lookup precedence, special-casing only the deployed teamai stub if necessary.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
…by name

Review follow-up on Tencent#699.

- PACKAGED_SKILL_FILES lists every file a release ever wrote under skills/,
  as the union of `git ls-tree -r <tag> -- skills/` over all 91 tags. The
  prune removes those paths and the directories they leave empty; a file a
  member added is kept, its directory with it, and pull says which and why.
  The stub directory loses its six known references by name instead of
  "everything that is not SKILL.md". Python bytecode of a script we shipped
  counts as ours, so a __pycache__ does not strand the tree.
- locateSkill searches the team repo, then installed agents, then the
  package. A directory a member created under `codebase`, `default`,
  `learning` or `share` is the skill they asked about, and the recall gate
  does not apply to it.
- A guard test fails when a file ships under skills/ without being recorded
  in the manifest, which a later migration would otherwise leave behind.
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in da6df38.

P1 — deletion without proving ownership. Both prune paths now remove only files the CLI wrote. PACKAGED_SKILL_FILES lists every path any release ever shipped under skills/, built as the union of git ls-tree -r <tag> -- skills/ over all 91 tags: 12 files for team-wiki-codebase, 1 for teamai-share-learnings, 7 for teamai. The prune removes those paths, then the directories they leave empty. A file a member added is kept, its directory with it, and pull names that directory and says the packaged files went and the rest is theirs to delete. The stub directory loses its six known references/*.md by name; the blanket "everything that is not SKILL.md" is gone. Python bytecode of a script we shipped counts as ours, so a __pycache__ left by running the wiki scripts does not strand the tree.

A guard test walks the packaged skills/ directory and fails if a file ships without being recorded in the manifest, since that is exactly the file a later migration would leave behind on every machine.

Worth flagging separately: teamai-wiki was packaged in 0.13.0 and 0.16.x and has never been pruned, so those machines still carry it. I left it out of this PR deliberately, because widening a destructive set inside a PR blocked for destructiveness is the wrong place for it. Happy to open an issue.

P2 — aliases shadowed local skills. locateSkill now searches the team repo, then the installed agents, then the package. A directory a member created under codebase, default, join, learning, learnings or share resolves to their skill, with its own metadata and no recall gate. core and wiki still resolve to the package when nothing on the machine claims the name.

P1 — GitLab row. Still not run, and I want to be plain about why rather than restate it a fourth time: this machine has no GitLab credential of any kind. glab auth status returns 401, the git credential helper has nothing for gitlab.com, and SSH host key verification fails. I cannot manufacture one. Every path this PR changes runs after clone and auth, and is covered by the git and github rows. It needs either a token on this machine or a maintainer running pull / push --dry-run / recall disable / uninstall once.

Stale e2e count. Fixed in the description: the skill-serving e2e was 4 tests when that line was written and is 6 now. Both later rounds added a case and the count was not carried over.

Built CLI, isolated HOME, a machine that upgraded with the pre-stub trees and the member's own files mixed in:

pull    team-wiki-codebase   packaged SKILL.md, README.md, references/**, scripts/** and __pycache__ removed
                             references/methodology/my-notes.md kept, directory kept, warning names it
        teamai-share-learnings  nothing of the member's in it, so the directory is gone
        teamai/              references/setup-admin.md removed, references/team-playbook.md kept,
                             SKILL.md IDENTICAL to packaged
skill show codebase          [local-only], "my own codebase skill", the member's path
skill show share             [local-only], exit 0, recall gate does not apply
skill show core              [builtin], the packaged skill-data/core/ path

Runs on da6df38: npx tsc --noEmit clean; npm run build ok; npx vitest run 3773 passed, 1 skipped, the same 3 pre-existing shell-profile Windows cases that fail on origin/main on macOS; skill-serving e2e 6 passed. The four new tests were confirmed to fail against the previous revision's sources before the fix was restored. The full e2e config also reports opencode-recall.test.ts failing to start, because node_modules/opencode-ai is not installed in this checkout; that file is untouched by this PR.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Recall gating is bypassed by the unnamed path command. src/skill-content.ts:358 prints the entire skill-data root without checking recall. After skill list --json reveals share, an agent can read <printed-root>/share/SKILL.md directly even though skill get share and skill path share refuse it. Require an explicit skill name or otherwise avoid exposing the gated directory.

  • [P1 blocking] Codex’s shared stub survives uninstall. src/builtin-skills.ts:38 makes teamai uninstallable, but deployBuiltinSkills can place it in .agents/skills/teamai, while discoverToolResources only scans the configured .codex/skills root at src/uninstall.ts:326. Include Codex’s shared root in uninstall discovery, as the legacy-pruning path already does.

  • [P1 blocking] The required provider test matrix is incomplete. The PR description explicitly says the GitLab row was not run. The repository’s pre-PR requirements call for real-CLI verification across git, gitlab, and github; Git and GitHub evidence alone does not satisfy that rule.

  • [P2 non-blocking] The served instructions state the wrong default. skill-data/core/SKILL.md:63 says team sharing is enabled by default, but getRecallSharing() defaults recall to false, and the PR description also states fresh teams have recall off. This can direct users toward a workflow that immediately refuses to run.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
Review follow-up on Tencent#699.

- `skill path` takes a name, always. The argument-less form printed the
  `skill-data/` root, and `<root>/share/SKILL.md` is readable from there —
  the content the gate withholds one command over.
- uninstall discovers skills in Codex's shared `.agents/skills` root, where
  resolveSkillDestination puts the stub whenever the skill already lives
  there. Without it, uninstall reported success and left it behind. Codex
  only, as the legacy prune already does.
- core/SKILL.md said team sharing is enabled by default; getRecallSharing
  defaults it to false. It now says recall is off by default and names
  `teamai recall enable`.
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 03f7611. All four addressed, and the GitLab row is finally run.

P1 — skill path with no name printed the gated root. The argument is now required: skill path <name>, skillPath(name: string), and the branch that printed skills/ and skill-data/ is gone. Nothing documented or tested used the argument-less form — the usage guides, the stub and every reference already pass a name — so the only caller it served was the one that read <root>/share/SKILL.md past the gate. The generated commands.md regenerates to <name> from Commander, so the drift guard carries it. Design doc says why there is no argument-less form.

P1 — Codex's shared stub survived uninstall. discoverToolResources adds SHARED_AGENT_SKILLS_PATH to the skill roots when tool === CODEX_TOOL, which is where resolveSkillDestination puts the stub whenever the skill already lives in the shared root. Codex only, same rule as the legacy prune. Test in uninstall.test.ts: ~/.agents/skills/teamai goes, ~/.agents/skills/my-own-skill stays; confirmed to fail on the previous revision by reverting the one line.

P2 — the served instructions stated the wrong default. core/SKILL.md said team sharing is enabled by default; getRecallSharing returns false. It now says recall is off by default and names teamai recall enable, which is the message skill get share actually prints.

P1 — the GitLab row. Run, against a real private gitlab.com project created for the run and deleted after it. provider: gitlab in teamai.yaml, built CLI, isolated HOME, Claude and Codex installed, the pre-stub trees seeded with a member's own file mixed into one:

pull                     my-skill synced; team-wiki-codebase kept and named (holds the member's
                         references/methodology/my-notes.md); teamai/SKILL.md deployed to
                         .claude/skills and .codex/skills, IDENTICAL to packaged
skill path                (no name)   exit 1, "missing required argument 'name'"
skill path share          exit 1      "share needs recall, which is disabled for this team"
skill path wiki           exit 0      prints skill-data/wiki
recall status             disabled    team sharing.recall.enabled false, no user override
skill get share           exit 1      | skill get --all  stderr "Skipped share"
skill show share          exit 1
push --all --dry-run      offers teamai-workflow only (the member's); no CLI-owned tree
recall disable            teamai-share-learnings removed; team-wiki-codebase, my-skill, teamai kept
uninstall --force         6 skill dirs removed, including .agents/skills/teamai;
                          .agents/skills/my-shared-skill and .claude/skills/teamai-workflow remain

The same matrix was re-run on the generic git provider against a local bare remote and passes identically. The PR description carries both.

What getting the GitLab row to run turned up, which is not this PR's to fix. glab's stored gitlab.com credential is an OAuth token. GET /api/v4/user returns 401 with a PRIVATE-TOKEN header and 200 with Authorization: Bearer, and src/providers/gitlab/gitlab-api.ts only ever sends PRIVATE-TOKEN. So a member who authenticated glab interactively — the normal way — has a working GitLab credential on the machine that teamai init cannot use, and the error they get says "Set the GITLAB_TOKEN environment variable", which does not tell them their existing login is the wrong token type. The matrix above works around it by driving git through glab auth git-credential and the project lifecycle through glab api. Happy to open an issue for accepting a Bearer token, or for detecting a glab login and saying so; it sits in auth, which this PR does not touch, and widening it here would repeat the mistake the prune set was blocked for.

Runs on 03f7611: npx tsc --noEmit clean; npm run build ok; npx vitest run 3774 passed, 1 skipped, the same 3 pre-existing shell-profile Windows cases that fail on origin/main on macOS; skill-serving e2e 7 passed, including the new skill path case.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:404 now classifies the entire teamai directory as removable, and src/uninstall.ts:794 recursively deletes it. Any user-added files preserved by the new deployment cleanup inside teamai/ are therefore lost during teamai uninstall. Remove only entries listed in PACKAGED_SKILL_FILES, then prune empty directories, as deployment already does.
  • [P1 blocking] The required provider test matrix is incomplete. The PR explicitly says the GitLab row was not run, while the repository requires real-CLI verification for git, gitlab, and github before PR submission.
  • [P1 blocking] skill-data/wiki/SKILL.md:310 and the phase files emit executable commands containing an unquoted, raw {SKILL_DIR}. After replacement, installations under paths containing spaces—and Windows paths used through Bash—produce invalid commands. Quote the placeholder in every shell command, e.g. python3 "{SKILL_DIR}/scripts/validate_kb.py" ....
  • [P2 non-blocking] skill-data/wiki/references/overview.md:3 still says the methodology, scripts, and agent specifications are deployed into agent directories. This directly contradicts the new design, where only the discovery stub is deployed and these files remain under skill-data/. Update the served reference to avoid sending agents to nonexistent paths.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
…LL_DIR}

Review follow-up on Tencent#699.

- uninstall removed a CLI-owned skill directory whole, undoing one command
  over the guarantee pull makes. It now removes the PACKAGED_SKILL_FILES
  paths through the same removeOwnedFiles, keeps a directory holding a file
  the member added, says which one, and tells the confirmation prompt so it
  no longer promises a directory it will keep. A team-repo skill is synced
  whole and still goes whole.
- Served shell commands quote the placeholder: `python3 "{SKILL_DIR}/..."`.
  Unquoted, an install path with a space ("Program Files", "Application
  Support", a Windows path through Bash) splits into two arguments and the
  documented invocation fails. A test fails on an unquoted occurrence after
  any command word, in SKILL.md or any reference.
- wiki/references/overview.md said the methodology, scripts and agent specs
  are deployed into agent directories. They are not: only the stub is, and
  the rest is served from the installed CLI.
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 9c21cc8.

P1 — uninstall deleted a CLI-owned directory whole. Correct, and it undid one command over the guarantee pull makes. The split is now by who owns the directory, not by which command is running:

  • a team-repo skill is synced whole, was never partly the member's, and still goes whole;
  • a CLI-owned one goes through the same removeOwnedFiles(dir, PACKAGED_SKILL_FILES.get(name)) that deployment uses — the packaged paths, then the directories they leave empty. A directory still holding a file the member added is kept and named.

The confirmation prompt was promising something it no longer does, so it says so per line now:

   Skills (6 directories):
     …/.claude/skills/my-skill
     …/.claude/skills/team-wiki-codebase   (TeamAI-packaged files only; anything you added stays)
     …/.claude/skills/teamai               (TeamAI-packaged files only; anything you added stays)
     …/.codex/skills/my-skill
     …/.codex/skills/teamai                (TeamAI-packaged files only; anything you added stays)
     …/.agents/skills/teamai               (TeamAI-packaged files only; anything you added stays)

✔ Removed 4 skill directories
⚠ Kept …/.claude/skills/team-wiki-codebase: it holds files TeamAI did not put there. …
⚠ Kept …/.claude/skills/teamai: it holds files TeamAI did not put there. …

Test in uninstall.test.ts: a stub directory holding references/setup-admin.md (packaged) and references/team-playbook.md (the member's), a legacy tree holding scripts/scan_repo.py and references/methodology/my-notes.md, and a teamai-share-learnings with nothing of theirs. The two member files and their directories survive, everything packaged goes, and the third directory goes whole. Confirmed to fail on the previous revision by disabling the one branch.

P1 — unquoted {SKILL_DIR} in executable commands. Fixed in all five: wiki/SKILL.md (the validate_kb.py row and the cp … project-overview.md line in the phase diagram), references/methodology/phase4-quality.md (two), references/phases/k1-reverse-engineering.md, references/phases/k4-quality.md. Prose and reference tables that only name a path are left alone — quoting a path inside a Markdown table cell would be noise, and nothing runs those.

Verified by running the two scripts as they ship from a path with a space, in both spellings:

SKILL_DIR = …/scratchpad/Program Files/teamai cli/skill-data/wiki

quoted (this PR)      python3 "$SKILL_DIR/scripts/validate_kb.py" "$OUT"     ran, exit 0
                      python3 "$SKILL_DIR/scripts/scan_repo.py" --help       ran, exit 0
unquoted (before)     python3 $SKILL_DIR/scripts/validate_kb.py $OUT
                        can't open file '…/scratchpad/Program': No such file or directory
                      python3 $SKILL_DIR/scripts/scan_repo.py --help
                        can't open file '…/scratchpad/Program': No such file or directory

A test in skill-content.test.ts scans every served SKILL.md and reference for a command word followed by a bare placeholder and fails with the file and line. Reverting one of the five makes it print wiki/references/phases/k4-quality.md:8: python3 {SKILL_DIR}/scripts/validate_kb.py <output_dir> --verbose.

P2 — wiki/references/overview.md still described the old deployment. Rewritten: the methodology, scripts and agent specifications are not copied into .claude/, .codebuddy/, .cursor/ or anywhere else; they ship inside the installed CLI and are served by teamai skill get wiki (--full for the references), which is why what an agent reads matches the CLI it is running. teamai skill path wiki prints the directory, for the commands above that run the scripts. Grepped the rest of skill-data/ for the same claim; that line was the only one.

P1 — the GitLab row. It is run. It was run before this round, on da6df38, and is in the PR description and in the previous comment; I have re-run it on this revision. A real private gitlab.com project, created for the run and deleted after it, provider: gitlab in teamai.yaml, the built CLI, an isolated HOME, Claude and Codex installed, the pre-stub trees seeded with the member's own files in them:

pull                     my-skill synced; team-wiki-codebase kept and named; teamai/SKILL.md
                         deployed to .claude/skills and .codex/skills, IDENTICAL to packaged
skill path                (no name)  exit 1, "missing required argument 'name'"
skill path share          exit 1     | skill path wiki  exit 0
recall status             disabled   (fresh-team default)
skill get share           exit 1     | skill get --all  stderr "Skipped share" | skill show share  exit 1
push --all --dry-run      offers teamai-workflow only; no CLI-owned tree
recall disable            teamai-share-learnings removed; the rest kept
uninstall --force         4 dirs removed incl. .agents/skills/teamai; teamai/ and team-wiki-codebase
                          kept and named, holding only the member's files

On the glab detour, since it is easy to read as a gap in this row. glab's stored gitlab.com credential is an OAuth token: GET /api/v4/user is 401 with a PRIVATE-TOKEN header and 200 with Authorization: Bearer, and src/providers/gitlab/gitlab-api.ts only ever sends PRIVATE-TOKEN. That affects teamai init against GitLab, and nothing else — it is the one command in the flow that calls the GitLab API to resolve and clone the repo. It does not affect the row above, because everything this PR changes runs after the repo is on disk: pull, the prune, skill get/path/show/list, push, recall, uninstall all read ~/.teamai/config.yaml and the local clone and touch no GitLab API. The row is therefore complete for what this PR changes — the team repo is a real private gitlab.com project with provider: gitlab, reached over the network by git the same way init would have left it.

What I cannot verify from here is the init call itself against GitLab, because doing so needs a Personal Access Token with api scope, and this machine has an OAuth login instead. I am not going to fake it or claim it. Two things follow, neither of them this PR's:

  1. A member who authenticated glab the normal interactive way has a working GitLab credential that teamai init rejects, with an error that says "Set the GITLAB_TOKEN environment variable" — which does not tell them their existing login is the wrong kind of token. Worth an issue: accept a Bearer token, or detect a glab login and say so.
  2. If a maintainer holds a GitLab PAT, one teamai init <gitlab repo> closes the last unverified call. Happy to add it to the description if someone runs it.

Widening this PR into GitLab auth to close that would repeat the mistake the prune set was blocked for, so I have left it out deliberately.

Runs on 9c21cc8: npx tsc --noEmit clean; npm run build ok; npx vitest run 3776 passed, 1 skipped, the same 3 pre-existing shell-profile Windows cases that fail on origin/main on macOS; skill-serving e2e 7 passed; the git and gitlab provider rows re-run against this revision.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] src/builtin-skills.ts:280 — In reporting-only HTTP mode, this deletes the legacy team-wiki-codebase/share skills and then skips deploying the replacement teamai stub at line 281. After an upgrade and pull, agents lose every discoverable built-in entry point, even though workflows such as the local wiki generator remain usable and HTTP mode is documented as a skill consumer. Either deploy the discovery stub in reporting-only mode or retain the legacy entry until a replacement exists.
  • [P2 non-blocking] src/index.ts:133 — The help text still calls teamai skill list an alias for teamai list skills --source all, but skillList() now additionally prints the CLI-served catalog. docs/usage-guide.md:452 repeats the inaccurate equivalence. Update the wording or preserve identical behavior.

The PR description includes a detailed test plan and real-CLI E2E records across the required agents and git, gitlab, and github providers, so no testing-description finding is needed.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
… from Codex

Review follow-up on Tencent#699.

- `skill show <served skill>` refuses a recall-blocked skill with the same
  message and exit code as `skill get` / `skill path`; it printed the
  directory those two withhold.
- A skill resolved from skill-data/ is classified `[builtin]` directly.
  BUILTIN_SKILL_NAMES only knows the deployed stub, so `skill show core`
  reported `[local-only]` beside a package path.
- pruneLegacyBuiltinSkills reaches `.agents/skills` only on Codex's own
  pass. Another enabled tool's pass deleted Codex's legacy copies while
  Codex was excluded, against the enabledAgents guarantee.
- The share skill and its references are written in English; the generated
  document still follows the session's language. The AGENTS.md exception
  for Chinese skill-data output is dropped.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
… push, wiki in English

Review follow-up on Tencent#699.

- resolveServableSkill is the only way to obtain a PackagedSkill outside
  skill-content.ts; it returns `blocked` instead of the skill, so `get`,
  `path`, `list` and `show` inherit the recall gate by construction.
- push never offers `team-wiki-codebase` / `teamai-share-learnings` as new
  user skills: between the upgrade and the first pull they are still on
  disk (isCliOwnedSkillName).
- `recall disable` removes the legacy `teamai-share-learnings` directory
  again (LEGACY_RECALL_SKILL_NAMES), skipping excluded agents.
- `skill list` prints the packaged catalog before `teamai init`, with a
  hint for the team half, instead of failing on the team listing.
- skill-data/wiki (SKILL.md, 14 references, 2 scripts) translated to
  English. Generated document names follow one glossary; validate_kb.py
  still recognises headings of knowledge bases built by the previous
  release, matched by code point so the source stays ASCII.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
…by name

Review follow-up on Tencent#699.

- PACKAGED_SKILL_FILES lists every file a release ever wrote under skills/,
  as the union of `git ls-tree -r <tag> -- skills/` over all 91 tags. The
  prune removes those paths and the directories they leave empty; a file a
  member added is kept, its directory with it, and pull says which and why.
  The stub directory loses its six known references by name instead of
  "everything that is not SKILL.md". Python bytecode of a script we shipped
  counts as ours, so a __pycache__ does not strand the tree.
- locateSkill searches the team repo, then installed agents, then the
  package. A directory a member created under `codebase`, `default`,
  `learning` or `share` is the skill they asked about, and the recall gate
  does not apply to it.
- A guard test fails when a file ships under skills/ without being recorded
  in the manifest, which a later migration would otherwise leave behind.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
Review follow-up on Tencent#699.

- `skill path` takes a name, always. The argument-less form printed the
  `skill-data/` root, and `<root>/share/SKILL.md` is readable from there —
  the content the gate withholds one command over.
- uninstall discovers skills in Codex's shared `.agents/skills` root, where
  resolveSkillDestination puts the stub whenever the skill already lives
  there. Without it, uninstall reported success and left it behind. Codex
  only, as the legacy prune already does.
- core/SKILL.md said team sharing is enabled by default; getRecallSharing
  defaults it to false. It now says recall is off by default and names
  `teamai recall enable`.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
…LL_DIR}

Review follow-up on Tencent#699.

- uninstall removed a CLI-owned skill directory whole, undoing one command
  over the guarantee pull makes. It now removes the PACKAGED_SKILL_FILES
  paths through the same removeOwnedFiles, keeps a directory holding a file
  the member added, says which one, and tells the confirmation prompt so it
  no longer promises a directory it will keep. A team-repo skill is synced
  whole and still goes whole.
- Served shell commands quote the placeholder: `python3 "{SKILL_DIR}/..."`.
  Unquoted, an install path with a space ("Program Files", "Application
  Support", a Windows path through Bash) splits into two arguments and the
  documented invocation fails. A test fails on an unquoted occurrence after
  any command word, in SKILL.md or any reference.
- wiki/references/overview.md said the methodology, scripts and agent specs
  are deployed into agent directories. They are not: only the stub is, and
  the rest is served from the installed CLI.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 22, 2026
…ist alias

Review follow-up on Tencent#699.

- Reporting-only HTTP pull pruned the legacy trees and deployed nothing, so
  a member on an HTTP team came out of the upgrade with no built-in entry
  point at all. The skip predates CLI-served content: it existed because the
  only deployable unit then needed a team repo. The stub does not — its
  workflows are printed by the installed binary, and `skill get wiki` is a
  local knowledge-base generator that never touches a repo. The stub now
  deploys in every mode, and `reportingOnly` goes with the branch it gated:
  nothing else read it.
- `teamai skill list` called itself an alias for `teamai list skills
  --source all`. It has not been one since it started printing the CLI-served
  catalog underneath. Both descriptions, the generated command reference and
  both usage guides now say what it does.

Refs Tencent#678
@SaulMoro

Copy link
Copy Markdown
Contributor Author

Follow-up in 292a732, rebased onto origin/main (9ce8a0e).

P1 — reporting-only pull pruned and deployed nothing. Correct, and the real before-state is worse than the finding describes. Reproduced with the built CLI on the parent revision (a84d544), an isolated HOME, teamai init --http … --token …, Claude and Codex installed, the pre-stub layout seeded:

parent revision (a84d544)   ~/.claude/skills/teamai/SKILL.md            "# old stub"
                            ~/.claude/skills/teamai/references/setup-admin.md   "# old ref"
                            (team-wiki-codebase and teamai-share-learnings pruned)

this revision (292a732)     ~/.claude/skills/teamai/SKILL.md            IDENTICAL to packaged
                            ~/.codex/skills/teamai/SKILL.md             IDENTICAL to packaged

So the member did not merely lose the legacy entry point: the teamai directory kept a pre-stub body and a stale reference forever, because the prune covers the legacy names and the clean-up of teamai/ lives on the deploy path that was skipped.

The skip predates CLI-served content. It existed because the only deployable unit then — teamai-share-learnings — needed a team repo. The stub does not: its workflows are printed by the installed binary, and skill get wiki is a local knowledge-base generator that touches no repo at all. So the stub deploys in every mode now, and reportingOnly went with the branch it gated — refreshTeamRepo no longer returns it and nothing else read it, so there is no mode left for a later change to skip in.

Verified against the fixed build, same HOME, no team repo on disk:

$ HOME=… teamai pull
✔ [user] Team repo: HTTP (report/sync delivery)
$ diff ~/.claude/skills/teamai/SKILL.md skills/teamai/SKILL.md && echo IDENTICAL
IDENTICAL
$ HOME=… teamai skill get wiki | head -2      # served with no repo present
---
name: wiki

The unit test in skip-uninstalled-tools.test.ts now asserts prune and deploy in the same pass (deployed is 1, the shared .agents legacy tree is gone, .codex/skills/teamai/SKILL.md is byte-identical to the packaged stub) instead of asserting the skip.

P2 — teamai skill list is no longer an alias. It stopped being one when it started printing the CLI-served catalog underneath. Both descriptions now say what the command does, skill-data/core/references/commands.md was regenerated from the Commander table rather than edited, and both usage guides follow:

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

Rebase. origin/main moved six commits ahead, and #722 slimmed all five READMEs into a landing page, moving the Stop-hook section this branch edited into docs/product-overview.md. The five README hunks resolve to main's side — the section no longer exists there — and the same two edits were applied where the text now lives, in docs/product-overview.md and docs/product-overview.zh-CN.md. No ja/ko/th product-overview exists, so nothing was orphaned. docs/usage-guide.* and src/pull.ts auto-merged.

Runs on 292a732:

npx tsc --noEmit         clean
npm run build            ok
npx vitest run           3812 passed, 1 skipped, 273 files
                         3 failures, all in shell-profile.test.ts (Windows cases), which fail
                         the same way on origin/main on macOS — pre-existing, unrelated
npm run test:e2e         178 passed, 27 skipped, 43 files
                         (opencode-recall needed `npm ci` in this worktree first; it had no
                         node_modules/opencode-ai, so the earlier run errored in beforeAll)

git provider row re-run against this revision, built CLI, isolated HOME, local bare remote with provider: git, Claude, Codex, CodeBuddy and OpenCode installed, the pre-stub trees seeded with the member's own files and a user-created teamai-workflow beside them:

pull                      my-skill synced to all four; teamai/SKILL.md IDENTICAL to packaged in
                          .claude, .codex, .codebuddy, .config/opencode; teamai-share-learnings and
                          the stale teamai/references gone; team-wiki-codebase kept and named
                          (holds references/methodology/my-notes.md)
skill list --json         core, setup, wiki served; share blockedByRecall true, path null
skill get share           exit 1  |  skill path share  exit 1  |  skill get --all  stderr "Skipped share"
recall enable             skill get share serves the skill
push --all --dry-run      offers teamai-workflow only; no CLI-owned tree
uninstall --force         8 dirs removed; team-wiki-codebase kept and named; the member's
                          my-notes.md and teamai-workflow survive

gitlab and github rows stand from the previous revision: this round changes the reporting-only branch of deployBuiltinSkills, two Commander descriptions and the generated reference, none of which is provider-dependent, and the rebase's six upstream commits are covered by the suites above.

…LL_DIR}

Review follow-up on Tencent#699.

- uninstall removed a CLI-owned skill directory whole, undoing one command
  over the guarantee pull makes. It now removes the PACKAGED_SKILL_FILES
  paths through the same removeOwnedFiles, keeps a directory holding a file
  the member added, says which one, and tells the confirmation prompt so it
  no longer promises a directory it will keep. A team-repo skill is synced
  whole and still goes whole.
- Served shell commands quote the placeholder: `python3 "{SKILL_DIR}/..."`.
  Unquoted, an install path with a space ("Program Files", "Application
  Support", a Windows path through Bash) splits into two arguments and the
  documented invocation fails. A test fails on an unquoted occurrence after
  any command word, in SKILL.md or any reference.
- wiki/references/overview.md said the methodology, scripts and agent specs
  are deployed into agent directories. They are not: only the stub is, and
  the rest is served from the installed CLI.
…ist alias

Review follow-up on Tencent#699.

- Reporting-only HTTP pull pruned the legacy trees and deployed nothing, so
  a member on an HTTP team came out of the upgrade with no built-in entry
  point at all. The skip predates CLI-served content: it existed because the
  only deployable unit then needed a team repo. The stub does not — its
  workflows are printed by the installed binary, and `skill get wiki` is a
  local knowledge-base generator that never touches a repo. The stub now
  deploys in every mode, and `reportingOnly` goes with the branch it gated:
  nothing else read it.
- `teamai skill list` called itself an alias for `teamai list skills
  --source all`. It has not been one since it started printing the CLI-served
  catalog underneath. Both descriptions, the generated command reference and
  both usage guides now say what it does.

Refs Tencent#678
Tencent#724 landed `skills/teamai/references/provider-tgit.md` and repointed
setup-admin.md and join-member.md at it. Rebasing onto that left the new
file in a tree this branch no longer deploys, and the pointers in bare
`provider-tgit.md` form the served skills do not use.

- move it to `skill-data/setup/references/`, beside the two files that
  cite it, so `teamai skill get setup --full` serves it
- rewrite every pointer to it as `{SKILL_DIR}/references/provider-tgit.md`
- list it in the setup skill's reference table
- add `references/provider-tgit.md` to PACKAGED_SKILL_FILES, so the prune
  removes it from members who pulled a release that shipped it
…afe casts

Review findings from the standards axis, plus the doc half of the prune
count.

- `SkillCatalogEntry` allowed `{blockedByRecall: true, path: '/…'}`, an
  invariant `skillCatalog` then upheld by hand. Split it on
  `blockedByRecall`, so the withheld directory is a type error rather than
  a review catch. Both variants keep the `path` key, so the
  `skill list --json` shape is unchanged.
- `command.commands as Command[]` stripped commander's `readonly` in three
  places. `for…of` and `.find` need no cast.
- `docs/designs/skill-serving.md` still said the prune removes six
  `teamai/references/*.md`; provider-tgit.md makes it seven.
Publishing a skill is `teamai push --skill`, which never consulted recall
(`src/push.ts` names it nowhere). On main the flow shipped in the teamai
skill, ungated. Moving `contribute-member.md` under `share` put it behind
the recall gate, so with recall off — a new team's default — the core
routing table sent the agent to `teamai skill get share`, which exits 1
and tells it to enable recall. Wrong advice for a flow recall does not
touch, and no other path to the instructions.

Move the file to `core`, the skill that already owns `push`, and split the
routing row so publishing and session learnings stop sharing one
destination. The gate itself is right and stays: learnings do need recall.

`share/SKILL.md` already called this "a different flow"; now it points at
`teamai skill get core --full` instead of at its own references.

PACKAGED_SKILL_FILES is unchanged: the legacy path a pre-stub release
wrote is still `teamai/references/contribute-member.md`.
…door

Review finding: `removeOwnedFiles` proves ownership by pathname and
deletes without reading the file, so a member's edit goes with it.

For a path the current package still ships that changes nothing: the old
deployment overwrote it with `overwrite: true` on the same three triggers,
so the edit died either way, at the same moment. The case the objection
gets right is a path a retired release shipped and the package no longer
does — the overwrite never reached it, so the edit did survive, and the
prune is the first thing to remove it.

Copy every pruned file to `~/.teamai/removed-skills/<date>/<tool>/<skill>/`
before removing it. Outside every agent directory, so nothing reads it back
as a skill.

Verifying contents against a hash of each released version was the other
way out, and it is worse: anything not byte-identical is then kept, so one
CRLF checkout on Windows — a platform this project supports — leaves the
whole 176 KB in place and reports success. Backing up gives the same
guarantee without betting the migration on byte equality.

Uninstall keeps deleting outright: there the member asked for the files to
go.
… its own

Two holes in the backup the previous commit added, both reported in review.

The copy's failure was swallowed at debug level and the delete went ahead
regardless, so a full disk or a read-only home turned the migration back
into the data loss the backup exists to prevent — and the log still named
a backup directory that held nothing. A file whose copy fails is now kept,
counted, and named at warn level; `removeOwnedFiles` returns what happened
instead of a bare boolean, and only a run that copied something names the
directory.

The backup path was `<date>/<tool>/<skill>` with `overwrite: true`, so the
second copy of a name silently replaced the first. Codex prunes the same
skill from `.codex/skills` and the shared `.agents/skills`, and two pulls
share a date. The path now carries a per-run id and the skill root, and the
copy refuses to overwrite rather than clobbering a copy it cannot replace.

Tests cover both: a file where the backup tree must start makes every copy
fail, and the two Codex roots land in separate directories. Each fails
against the previous commit.
Three review findings, all in the prune.

A symlinked skill directory was walked through. `readdir` follows the link,
every path under it matches a packaged name, and the delete lands in
someone else's checkout. Ownership now stops at the link: the root is
lstat'd, a symlink is refused, and link and target are left alone.

The stub directory was pruned against the full historical file list, which
includes the SKILL.md written one line later. Deployment runs on every
session start, unchanged revision included, so that archived an identical
copy per session forever. Only paths this release no longer ships are
archived now.

Backups were written under the tool's base directory, which under project
scope is the repo root, so they landed in the working tree outside the
generated .teamai/.gitignore. They go to the machine's home.

Also: `skill show <packaged>` resolved the team before the package, so it
failed on a machine that never ran `teamai init` for content that needs no
team. Packaged names resolve first and print without the team-dependent
fields.
…prune

Findings from a self-review run before pushing, plus the two from the last
review round.

The symlink guard was one level too low. It lstat'd the skill directory, so
the common shape — `~/.claude/skills` itself linked at a dotfiles checkout —
walked straight through: every directory under the link is real. The guard
now walks each component below the tool's base directory and stops at the
first link, which covers the prune and the stub write with one check.
Components at or above the base are not checked: a home directory under a
link is ordinary, and refusing there would disable deployment on those
machines.

The symlink branch borrowed the foreign-files message, so a member was told
"delete the rest yourself" about a directory nothing had touched. Following
that destroys what the guard just protected. It has its own sentence now, in
pull and in uninstall.

`remove()` was not fail-closed the way the backup is: a read-only parent
left the tree half-pruned under a debug line, and a `walkFiles` that threw
returned success. Both are recorded in `notRemoved` and reported.

The backup path gained the base directory: `inheritUserScope` deploys the
user base and then the project base in one process, same tool, same root,
same skill name, and `errorOnExist` turned that collision into files the
second pass could neither archive nor prune.

Docs corrected against the code: the archive path, the tag count (98, not
91, and `teamai-wiki` is excluded), the version line, and the size table.
…assify legacy names as ours

The Stop-hook hint said "run /teamai", but bare /teamai prints the menu and
stops, so following the primary suggestion never reached the share workflow.
It now names an invocation the core skill routes to share, with the
`teamai skill get share` fallback kept. The four docs that quote the hint follow.

The setup skill sent "publish one skill" to `teamai skill get share`, which
handles session learnings and is refused when recall is off (the default);
reusable-skill publishing lives in core's contribute-member reference and needs
no recall. The routing row and the two references that repeated it now point
there.

classifySkill checked BUILTIN_SKILL_NAMES alone, so until the first pull pruned
them, team-wiki-codebase and teamai-share-learnings showed as [local-only]. It
now uses isCliOwnedSkillName, the rule push and uninstall already apply.
…e out of the tarball, report a failed uninstall delete

A review of the whole branch against Tencent#678, Tencent#730 and the design doc, run before
pushing. What it found and what changed:

- The Stop-hook share reminder was gated on the hint switch alone; recall is off
  by default and `teamai skill get share` refuses then, so the reminder pointed
  at a command that said no. It is withheld while recall is off, the same gate
  the workflow has; the served text about when the prompt appears now matches.
- `npm pack` swept `skill-data/wiki/scripts/__pycache__` into the tarball once
  the e2e suite had run the scripts. Excluded in package.json "files", asserted
  absent in the tarball test, and the e2e run sets PYTHONDONTWRITEBYTECODE.
- The `share` description still offered to publish reusable skills, the flow its
  own body sends to `core`; the sentence is gone.
- `skill show <unknown>` before `teamai init` threw the init error as a stack
  trace; it prints the not-found line and exits 1.
- The stub pre-approved every `teamai` command from the always-loaded unit;
  narrowed to `Bash(teamai skill:*)`, which is all it asks for (Tencent#678).
- Six routing lines loaded `core --full` to reach one reference; they name the
  file under `$(teamai skill path core)/references/` instead.
- `uninstall` reported a failed delete as "holds files TeamAI did not put there;
  the packaged files were removed", both false and the error unprinted. It names
  the file and the error; a test makes the stub directory read-only.
- The stub directory archived under `<tool>/.claude-skills-teamai/teamai/` while
  the legacy trees used `<tool>/.claude-skills/<skill>/`; one layout now.
- CHANGELOG entry; dead `isRecallEnabled` import; wiki heading still naming
  `team-wiki-codebase`; JSDoc on the wrong declaration; stale byte counts; the
  usage guides gain the recall refusal and the archive location; the design doc
  records the `--json` deviation, the legacy-name classification rule, the
  uninstall symlink scope and the fail-open wording.
…ller, withhold share from read-only sources

The reviewer flagged Chinese in skills/ and skill-data/ a third time. Both
reach the agent as CLI output, so the stub's trigger keywords, the paired
sample invocations and the Chinese name for TGit go; the agent translates for the user. A test
fails on CJK anywhere under either root.

Pre-push review of the whole branch, and what changed:

- uninstall walked through a linked ~/.claude/skills and deleted the
  packaged files inside the member's dotfiles checkout; pull refused the same
  layout. removeOwnedFiles now owns the guard, so pull, deploy and uninstall
  apply one check: the skills root and the skill directory. A linked
  ~/.claude (stow, chezmoi) is no longer refused, since every other resource
  writes through it and refusing left those machines on the pre-stub trees.
- share was served to read-only HTTP teams, where its last step
  (teamai contribute) always fails; reportingOnly used to skip it. The
  serving gate carries a reason (recall | read-only) with its own message,
  and `skill list --json` reports it as `blockedBy`.
- The bytecode rule claimed any file under any __pycache__; it now claims
  only the .pyc of a shipped script.
- recall disable pruned the shared .agents/skills root for an uninstalled
  Codex; it has deployment's install gate now.
- The source-team guard lost the legacy names when BUILTIN_SKILL_NAMES
  narrowed, so a source removal could delete a legacy tree wholesale.
- Routing: the admin wrap-up and the stub still sent "share what I learned"
  to share without saying it needs recall, and the stub filed "share this
  with my team" (the publish-a-skill phrase) under share. recall enable is
  described as the per-machine override it is, next to the team key.
- {SKILL_DIR} definitions now say how a reference file opened on its own
  spells the directory, since serving resolves the definition too.
- skill show: packaged resolve only when init fails, aligned label, a
  served skill is "served by the CLI, not installed".
- Docs: uninstall removes the archive with ~/.teamai; zh said the whole
  directory is kept; the product overview lacked the recall gate; the design
  doc's release, tag and byte figures were stale; CHANGELOG notes the
  language change of generated documents.
… uninstall too

The previous commit narrowed the guard to the skills root and the skill
directory, so a link at ~/.config or ~/.config/opencode was walked through:
the prune could delete, and deploy write, inside a dotfiles checkout. The
full walk from the tool's base directory is back, and removeOwnedFiles now
requires the base, so uninstall applies it too; each skill directory in the
uninstall plan carries the base its skills root hangs off.

A member whose whole ~/.claude is a link keeps the pre-stub trees and gets
the warning naming the path, as before the previous commit. Deleting through
a link is the one thing the prune must never do.
…cy names through the gate

contributeHintAllowed checked recall only. The dispatcher already drops this
gitOnly handler for HTTP teams, but the gate now says so itself, so the
reminder never points at a `share` that refuses as read-only wherever it runs.

`skill show teamai-share-learnings` searched the agent directories before
the package, so a legacy tree a pull had not pruned yet was shown with its
path while the gate refused `share`. A legacy built-in name now skips the
agent search and goes to the packaged skill and its gate; ordinary names and
aliases such as `share` keep a member's own directory first.
deployBuiltinSkills joined baseDir with the configured skills path, while
team-skill sync resolves the directory through skillsDirForTool: OpenClaw's
workspace, and HERMES_HOME for Hermes. Those agents got the stub in a
directory they never read and had their legacy trees pruned from the wrong
place. Deploy, the legacy prune, recall disable and uninstall now resolve
the same directory; the link guard starts at the tool's base directory when
the skills directory sits under it, else at that directory's parent.
…ad logs off stdout, drop inert allowed-tools

- A skills directory outside the tool's base (HERMES_HOME, an OpenClaw
  workspace) had the guard start at the root itself, so a linked root was
  never checked. It starts one level above now, and a linked HERMES_HOME is
  refused like a linked ~/.claude.
- The share gate loads the config, which can migrate it and report that with
  log.info on stdout: an upgrading machine got that line in `skill get`
  output and in `skill list --json`. Config loading reports on stderr for
  that call; setStderrOnly returns the previous mode so it can be restored.
- `allowed-tools` in the served skills was printed as command output and
  never processed as skill metadata, so it granted nothing. Removed, and the
  test now fails if one comes back. Only the stub's line pre-approves.
…ILOT_HOME is refused

skillsGuardBase started at the tool's base directory, which for Copilot in
user scope is COPILOT_HOME, so the walk never checked whether COPILOT_HOME
itself was a link, and pull and uninstall wrote and pruned through it. The
guard now starts at the scope root (home, or the project root), where a link
at or above is ordinary, in deploy, the legacy prune and uninstall alike; a
root configured outside it still has the walk start just above that root.
…a broken config, quote skill paths

- share and wiki had moved generated learnings and knowledge-base documents
  from always Chinese to the session language. Serving the instructions from
  the CLI does not need that, so both say "Simplified Chinese" again, in an
  English instruction; the CHANGELOG entry follows.
- skill show and skill list treated every autoDetectInit failure as "not
  initialized" and pointed at `teamai init`. requireInit now throws a tagged
  NotInitializedError; only that falls back to the packaged catalog, and a
  malformed or unreadable config propagates.
- `$(teamai skill path …)/…` is word-split in a shell command like an
  unquoted {SKILL_DIR}; all ten occurrences are double-quoted and the quoting
  test covers the form.
…issing

loadLocalConfig returns null both for a missing file and for one that fails
to parse, validate or migrate (it logs the reason). requireInit turned every
null into NotInitializedError, so skill show and skill list still fell back
to the packaged catalog and a `teamai init` hint on a broken config. Only an
absent file is NotInitializedError now; an existing one that could not be
used is an error naming its path, in requireInit and the user branch of
requireInitForScope. Covered through the real loader and the built binary.
…econd Codex copy

- The legacy prune and uninstall removed any file at a path a release had
  packaged, so a member's edit, a skill of their own under an old name, or a
  root TeamAI never managed (toolPaths or HERMES_HOME moved) lost its files.
  A file is ours now only at a packaged path and with content a release
  shipped there: PACKAGED_SKILL_DIGESTS records the sha256 of every blob over
  all 99 tags through v0.25.0 and main before the stub, 37 versions across 21
  paths. A skill-root SKILL.md is compared by its body, since releases before
  0.17 shipped no frontmatter and the deploy of the day repaired it on disk.
  The current stub is ours by the packaged copy. Anything else stays.
- Codex reads .codex/skills and the shared .agents/skills, and the stub goes
  to the shared one when a copy lives there; the copy an earlier release left
  in the other root kept its old SKILL.md and references. It is retired by
  the same ownership rule, archived first, and named when kept.
- Tests mock the digest table with a stand-in for shipped content, and a test
  keeps the stand-in on the same paths as the real table.
…the rebase

Tencent#713 and Tencent#736 edited skills/teamai/references/*.md, which this branch moved to
skill-data/setup/references/. Two hunks did not follow the move:
- join-member.md: TGIT_TOKEN is REST-API-only and cannot clone (Tencent#713).
- setup-admin.md: the /teamai share entry publishes a reusable skill; a
  session's learnings are automatic (Tencent#736), in English as the served text is.
Tencent#739's partial config mock is restored in skip-uninstalled-tools.test.ts.
…ig, drop hidden commands from the reference

- Legacy trees were pruned before the stub was written, so a refused or
  failed stub (a link, a read-only directory) left the agent with nothing
  to discover. They go only once the stub deployed for that agent.
- The share gate failed open on any config error. Only a machine with no
  config (NotInitializedError) is served; a config that exists but cannot be
  loaded blocks with its own reason, `blockedBy: "config"`.
- The KB template told agents to run `code-to-knowledge --update`, which
  does not exist; it names `teamai codebase --extract … --incremental`.
- The generated command reference listed hidden hook plumbing (`track`,
  `contribute-check`, `todowrite-hint`, …). It renders what `--help` lists.
- removeEmptyDirs swallowed every rmdir error, so a directory that stayed
  could be reported removed. Only "still holds something" is expected; any
  other failure is reported.
…e effects before the link guard

Review of 327f9cd:
- SKILL.md was compared by its body, so a member who changed only its
  frontmatter lost the file. Every release from 0.16.1 (the first whose
  deploy repaired frontmatter) shipped complete frontmatter, so what is on
  disk is what was shipped: digests are whole files now (42 versions over
  100 tags and main). A link is never ours; bytecode is ours only beside a
  script proven ours by content, decided before anything is removed.
- The stub dir's retired references were pruned before SKILL.md was copied;
  a failed copy left the old skill pointing at files that were gone. The
  stub is written first.
- The Codex destination was resolved with the reconciliation that deletes a
  duplicate, before the link guard ran. It is resolved side-effect free; the
  other copy is handled under the guard by retireOtherCodexCopy, whose
  report now names a failed backup or delete as such.
- A broken project config was skipped by detection, so the share gate
  answered with the user config. findUnreadableProjectConfig reports it via
  an optional sink on detection (no caller changes), and the gate blocks.
  The Stop-hook reminder is withheld on an unloadable config too.
- init announced the stub as ready when nothing was deployed; hook-dispatch
  is hidden (hook plumbing), and the reference says it lists public commands;
  the design doc no longer says teamai-workflow/teamai-import are removed.
… a fallback loads

findUnreadableProjectConfig dropped a recorded error whenever detection
went on to find a later candidate: a broken partition config followed by a
valid legacy .teamai/ config returned null, and the share gate answered with
the fallback's team. It now reports the first unreadable file regardless.
An existing config file that is empty or cannot be read is reported to the
sink too, instead of returning without a word.
@jeff-r2026
jeff-r2026 merged commit ca6e512 into Tencent:main Sep 23, 2026
9 of 10 checks passed
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 23, 2026
Reconcile the manifest namespace guard with Tencent#698 (push placement for rules
and agents) and Tencent#699 (skills served from skill-data/).

- Keep main's RolesManifestNotFoundError name with this branch's stricter
  loader (readManifestFile: only ENOENT without a dangling link is absent).
- Import isSafeNamespaceSegment and NAMESPACE_RULE from manifest-schema in
  push.ts and push-namespaces.ts; drop the old ASCII wording from their
  namespace errors.
- Port the role-id guards into main's placement: the legacy fallback with no
  roles.yaml and the silent default both refuse an unsafe role id.
- pushCore reports a failing scan (e.g. an unparseable roles.yaml) with exit 2
  instead of an uncaught stack trace; a projects manifest that cannot load
  for --project does the same. A legacy role that could not be resolved no
  longer places a new rule or agent at the shared root.
- The skills scan rethrows only manifest load failures again; a valid
  manifest that no longer lists the role keeps the role-id fallback.
- The learnings index uses the namespace rule contribute writes with, so a
  non-ASCII namespace is indexed.
- Namespace errors quote the value and manifest read errors say what to do.
- Docs, CHANGELOG and skill-data (troubleshooting, manage-admin) describe the
  broken-manifest behavior, including that --role cannot bypass it.
jeff-r2026 pushed a commit that referenced this pull request Sep 23, 2026
…eploy (#747)

* fix(skills): one share gate, actionable refusals, and a louder stub deploy

Follow-ups from the review of #699:

- The Stop-hook reminder and `teamai skill get share` ask one gate
  (`shareGate`, through `contributeHintAllowed`). The hook skipped the
  unreadable-project-config check, and the legacy `teamai contribute-check`
  command, still called by hooks written before the dispatcher, checked
  nothing, so both nudged towards a command that refused.
- The gate reads only a config load failure as "cannot be loaded"; any other
  fault propagates (the hook withholds the reminder and logs it at debug).
- A `config` refusal says what failed (the file and position for a parse
  error) instead of pointing at `teamai doctor`, which cannot see a broken
  config. `skill show` now refuses through the same helper, so its hint moves
  from stdout to stderr like `skill get` and `skill path`.
- `pull` warns when the discovery stub cannot be deployed (it was an empty
  catch on the fast path and a debug line on a full sync), and so does the
  legacy prune.
- Error text no longer claims a reason was logged when none was: an empty
  config is named as empty, and `init` points at ~/.teamai/debug.log, where
  every path that deploys nothing now records why.
- `core` routes a bare `/teamai` right after a friction reminder to `share`,
  as the stub already said.
- The command drift guard rejects an unknown subcommand inside a group
  (`teamai skill gett core` passed before).
- The contribute-check e2e asserts the reminder's real text again; the usage
  guides (EN, zh-CN) and the design doc cover the config refusal, the gate and
  the reminder routing.

* test(learnings): retry temp-dir cleanup that races a detached git gc

A push into the bare origin can leave `git gc --auto` writing to
objects/pack after the test returns; the single rmdir in afterEach then
fails with ENOTEMPTY (seen on CI, Node 22 ubuntu, #747).

* fix(skills): gate skill show before its lookups, name the failing field

Review of #747:

- `skill show share` under a broken project config searched the user
  config's team repo and agents, which detection falls back to, and printed
  a `share` found there. It now asks the gate first and refuses on a config
  block before any lookup. With an empty user config it refuses instead of
  ending in a stack trace.
- A config that parses but fails validation reported the Zod JSON dump,
  whose first line is `[`, so the refusal said `config.yaml: [.`. Every
  config loader now reports each issue as `field: reason` on one line.
- The docs and skills that describe the share reminder or the refusal say
  it is withheld on a read-only source and while the config cannot be
  loaded, and that a validation failure names the field: product-overview
  and usage-guide (EN, zh-CN), designs/skill-serving.md, core/SKILL.md,
  contribute-member, setup-admin, join-member and manage-admin.

* fix(skills): no share reminder where teamai is not set up

`contributeHintAllowed` fell open with no config at all, so a caller other
than the dispatcher (the legacy `teamai contribute-check`) still nudged in
projects that never set up teamai, which have no team to share with
(#748). It now returns false there. Serving the skill stays fail-open.

* fix(contribute-check): gate the legacy reminder on the session's cwd

`teamai contribute-check --stdin` asked the share gate about the directory
the hook process started in, while the session analysis used the payload
cwd. Started outside the project, it could read the user config and nudge
where `teamai skill get share` refuses (a project config that does not
load). It now moves to the payload cwd first, as hook-dispatch does.

* fix(pull): keep a debug.log record when the stub cannot be deployed

The previous commit turned both deploy catches into `log.warn`, which is
muted in silent mode and never reaches debug.log, and a SessionStart pull
runs detached with its output discarded. So the automatic pull, the one
that deploys the stub for most members, lost the only persistent record
it had. Both catches now warn and write the same line to debug.log.

* fix(skills): skill show and list never answer for the fallback team

Known issues left by #747:

- `skill show <name>` and `skill list` on a config that exists but does
  not load ended in a Node stack trace, and under a broken project config
  they searched the user config detection falls back to: another team's
  repo and agents. Both now ask `detectTeam`, the one place that tells
  "this team", "no team" and "cannot tell, and why" apart (`shareGate` is
  built on it). Without a usable team, `show` answers from the package
  alone and `list` prints only the packaged catalog; both say what failed
  on stderr and exit 1.
- A teamai.yaml that exists but fails validation was reported as "not
  found. Check your repo path". It is now named as invalid, empty or
  unreadable, like the local config.

* fix(skills): the gate reads the session's directory, and no project config is skipped

Codex review of 5793758:

- A project-location config that is not `scope: project` (or omits
  `scope`, which defaults to user) was skipped without a word, so the gate
  read past it to the user config. It is now reported as unusable, unless
  it is the user config itself, as when running from HOME.
- The legacy `contribute-check` changed into the payload cwd and, if that
  failed, asked the gate about the directory the process started in. It
  now passes the payload cwd to the gate (`detectTeam(cwd)`), and a cwd
  that no longer exists holds no project config, so only the user config
  is asked, as #753 does.

* fix(logger): record warnings in debug.log

`log.warn` wrote to the console only and was muted in silent mode, so a
detached SessionStart pull, whose output is discarded, lost every warning:
the stub deploy failure and the legacy prune among them. Warnings now reach
debug.log like debug and error lines. `warnStubNotDeployed` drops the
second `log.debug` call, which printed the line twice under --verbose.

* fix(skills): the dispatcher gate reads the payload cwd; a symlink is not HOME

Codex review of b0583f5:

- The dispatcher's `contribute-check` and `pending-hint` handlers asked
  the gate about the process's directory, trusting hook-dispatch's
  `chdir`; when that failed, the launcher's config decided. They now pass
  `resolveHookCwd(stdin)`, as the legacy command does.
- The HOME exception for a non-project scope compared the config file's
  real path, so a project config symlinked to ~/.teamai/config.yaml passed
  for the user config. It is now decided by the project's location: its
  root is HOME.

* fix(skills): only a missing cwd falls back to the user config; load it once

Codex review of 15a5b5d:

- `detectTeam` read any failure to see the payload cwd as "deleted", so a
  cwd it could not open (no permission, a path through a file) fell back
  to the user config and could allow the reminder. Only ENOENT does now;
  anything else is `unusable` and withholds it.
- `skill show share` and `skill list` loaded the config twice, through the
  gate and then the team lookup, and reported a broken one twice. Both
  detect the team once and hand it to the gate.

* fix(logger): a file-only record instead of persisting every warning

b0583f5 made every `log.warn` append to debug.log, wider than the two
failures it was for, and it wrote unrelated subprocess errors to disk.
`log.warn` is console-only again; `log.persist` writes one line to
debug.log and never to the console. The stub deploy catches and the
legacy prune catch use both, so a detached SessionStart pull keeps the
record and --verbose prints it once.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants