Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2db6fc6
feat(push): place all distributed resources with project and role nam…
legend91019 Oct 19, 2026
fd2c98b
fix(push): harden namespace routing and review (#1101)
legend91019 Oct 20, 2026
b48ef1f
fix(push): read manifests after pull and guard capture/restore paths
legend91019 Oct 21, 2026
7961eb0
chore: trigger CI
legend91019 Oct 21, 2026
514e6c0
chore: nudge CI trigger
legend91019 Oct 21, 2026
e9164dc
merge: resolve conflicts with main (#1253, #1254, #1256)
legend91019 Oct 21, 2026
91209e5
fix(push): do not stage pre-placement paths as superseded files
legend91019 Oct 21, 2026
93809cd
fix: address review findings — binary copy, prompt namespaces, env pl…
legend91019 Oct 21, 2026
d77e8f0
fix: snapshot key mismatch and manifest-error namespace fallback
legend91019 Oct 21, 2026
07af57d
fix: address second re-review — reuse-path snapshot, env copy, manife…
legend91019 Oct 22, 2026
9909453
fix: revert scanTeamForPull change, fix rule names in review-branch i…
legend91019 Oct 22, 2026
95f37a3
fix(review): entry identities for hooks, wiki authoritative root, per…
legend91019 Oct 22, 2026
62fe916
merge: resolve conflicts with main (review roots, diagnostics list)
legend91019 Oct 22, 2026
db6b03b
fix(review): nested prompts are shared paths, docs identities keep ex…
legend91019 Oct 22, 2026
3a61393
fix(push): place nested prompts, and remember where a repo-backed pla…
legend91019 Oct 22, 2026
161016d
fix(push): reuse a placement record in env scans, and expire one whos…
legend91019 Oct 22, 2026
bb59199
fix(push,workflow): retire a placed env original, keep explicit promp…
legend91019 Oct 23, 2026
7e6dd4d
test(push): use .md scorer names in the placement samples
legend91019 Oct 23, 2026
c15069a
fix(push): compare a placed copy with what its push delivered
SaulMoro Oct 22, 2026
87bb24b
fix(push): judge a placed env file against what its push delivered
SaulMoro Oct 22, 2026
4c1d3bd
fix(push): require --role to name a declared namespace
SaulMoro Oct 22, 2026
f683be1
fix(contribute): use the learnings namespaces of the member's roles
SaulMoro Oct 22, 2026
634e6ee
fix(push): let a scan error before the refresh reach the pull failure…
SaulMoro Oct 22, 2026
ba89548
fix(push): say to merge a conflicting edit into the placed file
SaulMoro Oct 22, 2026
2ced2a9
docs(changelog): note the --role namespace check
SaulMoro Oct 22, 2026
0a2723c
feat(namespaces): resolve inactive scorers namespaces like prompts
SaulMoro Oct 22, 2026
bdd76d6
feat(contribute): accept an empty --namespace as the shared root
SaulMoro Oct 23, 2026
1debe94
test(push): push only the rule where --role names a knowledge namespace
SaulMoro Oct 23, 2026
2c0e940
test(push): keep a teammate's change from a self-mode checkout behind…
SaulMoro Oct 23, 2026
6c4bb84
fix(push): leave out team files main changed under a self-mode checko…
SaulMoro Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ All notable changes to this project will be documented in this file. See [standa
### Changed

- `teamai init` no longer pulls team skills, rules, agents or MCP before it exits. It still saves config and installs hooks. In project scope it creates a tool directory only for a tool named with `--agent` or picked in a terminal; otherwise that directory appears when the tool starts a session, and that session start pulls. `teamai pull` still does not create a missing tool directory. A tool that reads rules and MCP once, before its SessionStart hook, sees them from the following session (for [#1065](https://github.com/Tencent/teamai-cli/issues/1065)).
- `teamai push --role <ns>` fails with exit 2 when no role or project declares `<ns>` for that type, as `--project` already does. It used to write the files under `<ns>` anyway, where pull delivers them to nobody. A team with neither manifest still accepts any namespace (for [#1101](https://github.com/Tencent/teamai-cli/issues/1101)).
- `teamai contribute --namespace ""` writes to the shared root `learnings/`, also for a member whose default is a namespace. It used to be rejected, so such a member could not reach the shared root (for [#1101](https://github.com/Tencent/teamai-cli/issues/1101)).
- In single-repo mode, `teamai push` from a branch behind the default branch leaves out a team file under `.teamai/` that the default branch changed since and the member did not, and names it. It used to push the member's older copy over the teammate's change. When the member changed that file too, push stops with exit 2 and names it; merge the default branch and push again (for [#1101](https://github.com/Tencent/teamai-cli/issues/1101)).

### 💥 Breaking Changes

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ sharing:
.teamai/mcp/mcp.yaml
```

`teamai push` sends skills and rules. Commit `docs/`, `hooks/hooks.yaml`, `mcp/mcp.yaml`, and `models/models.yaml` yourself.
`teamai push` sends every distributed resource type — skills, rules, agents, docs, env, hooks, mcp, models, claudemd, learnings, wiki, workflows, prompts, and scorers — with `--project` and `--role` namespace placement.

### Git permissions

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ teamai contribute --file /tmp/session.md
teamai contribute --file /tmp/session.md --namespace payments
```

`--namespace` must be a learnings namespace this scope declares in `manifest/projects.yaml`.
`--namespace` must be a learnings namespace this scope's projects declare in `manifest/projects.yaml`, or its roles in `manifest/roles.yaml`. `--namespace ""` writes to the shared root `learnings/`.

To stop the prompt, set this in `teamai.yaml`:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ teamai push
teamai push --role pm
```

`push` fills in `name` and `description` when the frontmatter is missing. A new skill goes to `skills/<ns>/<name>/`, where `<ns>` comes from your role or the active projects. Several choices ask which one. `--role` picks one.
`push` fills in `name` and `description` when the frontmatter is missing. A new skill goes to `skills/<ns>/<name>/`, where `<ns>` comes from your role or the active projects. Several choices ask which one. `--role` picks one. When the team has `manifest/roles.yaml` or `manifest/projects.yaml`, a role or project there must declare it.

## Rules

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/zh-CN/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ sharing:
.teamai/mcp/mcp.yaml
```

skill 和 rule 用 `teamai push`。`docs/`、`hooks/hooks.yaml`、`mcp/mcp.yaml`、`models/models.yaml` 自己 commit。
`teamai push` 发送所有分布式资源类型——skills、rules、agents、docs、env、hooks、mcp、models、claudemd、learnings、wiki、workflows、prompts、scorers——并按 `--project` 和 `--role` 做命名空间放置。

### Git 权限

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/zh-CN/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ teamai contribute --file /tmp/session.md
teamai contribute --file /tmp/session.md --namespace payments
```

`--namespace` 必须是当前 scope 在 `manifest/projects.yaml` 里声明的 learnings namespace。
`--namespace` 必须是当前 scope 的项目在 `manifest/projects.yaml` 里、或其角色在 `manifest/roles.yaml` 里声明的 learnings namespace。`--namespace ""` 写入共享根目录 `learnings/`。

不想要这条提醒时,在 `teamai.yaml` 里写:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/zh-CN/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ teamai push
teamai push --role pm
```

没有 frontmatter 时,`push` 会补上 `name` 和 `description`。新 skill 写到你的角色或当前项目给出的 `skills/<ns>/<name>/`。有多个时会问。`--role` 指定一个。
没有 frontmatter 时,`push` 会补上 `name` 和 `description`。新 skill 写到你的角色或当前项目给出的 `skills/<ns>/<name>/`。有多个时会问。`--role` 指定一个。团队有 `manifest/roles.yaml` 或 `manifest/projects.yaml` 时,它必须由其中某个角色或项目声明。

## Rules

Expand Down
6 changes: 3 additions & 3 deletions skill-data/core/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Generated: do not edit by hand. Regenerate with
- `teamai push` — Push local resources to team repo
- `--all` — Push all without confirmation
- `--skill <path>` — Push a specific skill by path (e.g., ~/.claude/skills/hai/my-skill or skills/hai_dev/my-skill)
- `--role <id>` — Namespace for new skills, rules and agents (skills/<id>/, rules/<id>/, agents/<id>/). Without --role or --project, the namespaces of your role and active projects decide
- `--project <id>` — Target a project: each new resource goes to that project's namespace for its own type — skills, knowledge for rules, agents (from manifest/projects.yaml)
- `--role <id>` — Namespace for every new resource type (skills/<id>/, rules/<id>/, docs/<id>/, env/<id>/, etc.). When the team has manifest/roles.yaml or manifest/projects.yaml, a role or project there must declare it for each type it places. Without --role or --project, the namespaces of your role and active projects decide
- `--project <id>` — Target a project: each new resource goes to that project's namespace for its own type — skills, knowledge for rules, agents, learnings, docs, env, hooks, mcp, models, and more (from manifest/projects.yaml)
- `--branch <name>` — Push to this destination branch instead of a generated teamai/push branch

## pull
Expand Down Expand Up @@ -370,7 +370,7 @@ Generated: do not edit by hand. Regenerate with
- `teamai contribute` — Contribute session knowledge to team repo; projects list shows the default destination and allowed namespaces
- `--file <path>` — Path to the contribution document
- `--title <title>` — Title for the contribution document
- `--namespace <ns>` — Write to an active learnings namespace (default: the only active namespace, or the shared root when none or several are active)
- `--namespace <ns>` — Write to an active learnings namespace, or to the shared root with "" (default: the only active namespace, or the shared root when none or several are active)
- `--session-id <id>` — Session ID for dedup tracking
- `--scope <scope>` — Target scope: user or workspace

Expand Down
4 changes: 4 additions & 0 deletions skill-data/core/references/contribute-member.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ The doc lands in the team's `learnings/` and appears for teammates on their next
pushing it would revert a teammate's update. To publish an edit of it, bring
the current version in first (`git fetch origin && git merge origin/<default>`,
or copy the team's current file over it), redo the edit on top, and push again.
Any other team file under `.teamai/` that the default branch changed since
your branch left it is left out of the push when you did not edit it. A file
you edited too, a rule included but not a skill's file, stops the push with
exit 2; merge the default branch and push again.

## After contributing

Expand Down
2 changes: 1 addition & 1 deletion skill-data/setup/references/manage-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ for the real reset fallback. Removal refuses when that refresh also fails.

Every namespace that names a directory — `knowledge`, `skills`, `agents`, `env`,
`hooks`, `mcp`, `models`, `docs` and `wiki` in either manifest, and `learnings` in `projects.yaml` (a role's `learnings:` is
ignored and unchecked) — must be a single path segment: no `/`, `\`, `:` or control character, no trailing
not checked) — must be a single path segment: no `/`, `\`, `:` or control character, no trailing
`.` or space, and not a Windows device name (`CON`, `NUL`, `COM1`, …). `team-codebase`
cannot be a `docs` namespace (`docs/team-codebase/` is the legacy codebase output). Two
namespaces of one resource type may not differ only by case, across both
Expand Down
35 changes: 35 additions & 0 deletions src/__tests__/contribute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import type { LocalConfig } from '../types.js';

// ─── contribute.ts tests ────────────────────────────────────
// These test the contribute() function in isolation,
Expand Down Expand Up @@ -111,3 +112,37 @@ describe('contribute', () => {
vi.doUnmock('../config.js');
});
});

describe('the learnings namespace of a member whose role declares one (#1101)', () => {
it('is where a contribution lands and one --namespace accepts, as pull delivers it to that role', async () => {
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-contribute-role-'));
fs.mkdirSync(path.join(repo, 'manifest'), { recursive: true });
fs.writeFileSync(path.join(repo, 'manifest', 'roles.yaml'), [
'version: 1',
'roles:',
' - id: ops',
' description: ops',
' resources: { knowledge: [], skills: [], agents: [], learnings: [ops-notes] }',
'',
].join('\n'));
const localConfig: LocalConfig = {
repo: { localPath: repo, remote: 'local' },
username: 'alice',
scope: 'project',
primaryRole: 'ops',
additionalRoles: [],
projects: [],
};
try {
const { resolveLearningsSubdir } = await import('../contribute.js');
const { deliveredLearningsNamespaces } = await import('../workspace-children.js');
const { resolveResourceNamespaces } = await import('../resource-namespaces.js');

expect((await resolveResourceNamespaces(localConfig))?.activeNamespaces.learnings).toEqual(['ops-notes']);
expect(await resolveLearningsSubdir(localConfig)).toBe('ops-notes');
expect(await deliveredLearningsNamespaces(localConfig)).toEqual(['ops-notes']);
} finally {
fs.rmSync(repo, { recursive: true, force: true });
}
});
});
11 changes: 10 additions & 1 deletion src/__tests__/e2e/contribute-namespace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ describe('contribute --namespace through the built CLI (#916)', () => {
if (projects[0] === 'svc-a') expect(result.output).toContain('pass --namespace <ns>: svc-a, payments');
});

it('publishes to the shared root with an empty --namespace when the default is a namespace', () => {
setProjects(['alpha']);
const result = run(['contribute', '--file', note, '--title', 'narwhal', '--namespace', '']);
expect(result.code, result.output).toBe(0);
expect(result.output).toContain('Contributed: learnings/narwhal-');
const files = git(['ls-tree', '-r', '--name-only', 'teamai-learnings'], origin).split('\n');
expect(files.filter((f) => f.endsWith('.md'))).toEqual([expect.stringMatching(/^learnings\/narwhal-/)]);
});

it.each(['project', 'user'])('previews the selected %s namespace without filesystem writes', (scope) => {
if (scope === 'user') {
const config = YAML.parse(fs.readFileSync(configPath, 'utf8'));
Expand All @@ -145,7 +154,7 @@ describe('contribute --namespace through the built CLI (#916)', () => {
expect(snapshot()).toEqual(before);
});

it.each(['alpha-notes', '../escape', 'a/b', 'a\\b', ''])('rejects the unavailable namespace %j before writing', (namespace) => {
it.each(['alpha-notes', '../escape', 'a/b', 'a\\b'])('rejects the unavailable namespace %j before writing', (namespace) => {
const before = snapshot(sandbox, true);
const result = run(['contribute', '--file', note, '--namespace', namespace]);
expect(result.code, result.output).toBe(1);
Expand Down
34 changes: 34 additions & 0 deletions src/__tests__/e2e/push-active-namespaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,40 @@ describe('push places a new resource by the active projects (#1021)', () => {
});
});

describe('push --role places only where a role or project declares the namespace for that type (#1101)', () => {
it('pushes nothing for a namespace no role or project declares', () => {
writeFile(path.join(project, '.claude', 'rules', 'new-rule.md'), '# new rule\n');

const pushed = run(['push', '--all', '--role', 'rogue']);

expect(pushed.code, pushed.output).toBe(2);
expect(pushed.output).toContain('--role rogue');
expect(pushedFiles()).toEqual([]);
});

it('pushes nothing for a new skill under a namespace no role or project declares', () => {
writeFile(path.join(project, '.claude', 'skills', 'new-skill', 'SKILL.md'), skillMd('new-skill'));

const pushed = run(['push', '--all', '--role', 'rogue']);

expect(pushed.code, pushed.output).toBe(2);
expect(pushed.output).toContain('--role rogue');
expect(pushedFiles()).toEqual([]);
});

it('pushes nothing for a namespace declared for other types only', () => {
// platform declares skills, knowledge and learnings, not docs.
writeFile(path.join(project, '.teamai', 'team-repo', 'docs', 'guide.md'), '# guide\n');

const pushed = run(['push', '--all', '--role', 'platform']);

expect(pushed.code, pushed.output).toBe(2);
expect(pushed.output).toContain('--role platform');
expect(pushed.output).toContain('docs');
expect(pushedFiles()).toEqual([]);
});
});

describe('push stops when the active projects cannot be resolved', () => {
const unresolved: [string, () => void, string][] = [
['the projects manifest is gone', () => commitOnTeam('manifest/projects.yaml', null), 'no manifest/projects.yaml'],
Expand Down
9 changes: 5 additions & 4 deletions src/__tests__/e2e/push-sync-followups-823.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,15 @@ describe('pre-push sync in single-repo mode (#823 items 2 and 10)', () => {
expect(fs.readFileSync(activeRule(), 'utf8')).toBe(R1);
});

it('still lists a genuine edit of a stale .teamai/rules copy as modified (#823 item 10)', async () => {
it('refuses a genuine edit of a stale .teamai/rules copy rather than push it over the teammate\'s update (#823 item 10)', async () => {
await run(['pull']);
teammateLandsOnMain('.teamai/rules/team-rule.md', R2);
fs.writeFileSync(activeRule(), `${R1}\nA local edit.\n`);

const push = await run(['--dry-run', 'push']);
expect(push).toContain('[rules] team-rule (modified)');
expect(push).not.toContain(HELD);
const push = await runCLI(['--dry-run', 'push'], projectRoot, home);
expect(push.code, push.output).toBe(2);
expect(push.output).toContain('.teamai/rules/team-rule.md changed both here and on origin/main');
expect(push.output).not.toContain(HELD);
});

it('holds a stale .teamai/skills copy on a branch behind a teammate\'s update (#823 item 10)', async () => {
Expand Down
Loading
Loading