Description
Three code paths implement the namespace convention differently. filterRulesByKnowledgeNamespaces is the reference implementation: it matches the first path segment against the active namespace set, and ships any name without a segment to every member.
// src/pull.ts:249-255
if (slashIndex === -1) return true;
const namespace = rule.name.slice(0, slashIndex);
return knowledgeNamespaces.includes(namespace);
Two paths diverge from it. Neither emits a message when it does.
1. push does not apply --role or --project to rules or agents
--project resolves to the project's skills namespace and collapses into options.role (src/push.ts:292-320). Step 4 then applies that namespace to skills alone:
// src/push.ts:799-802
selectedItems.filter(i => i.type === 'skills' && i.status === 'new' && !i.namespace)
A rule pushed with --project front-app is written to rules/<item.name>.md (src/resources/rules.ts:154-155). That path carries no first segment, so the next teamai pull ships the rule to every member of the team.
<repo>/.claude/rules/my-rule.md project scope scans projectRoot (src/types.ts:1521-1532)
-> teamai push --project front-app flag applies to skills only (src/push.ts:799-802)
-> rules/my-rule.md written with no first path segment
-> teamai pull slashIndex === -1 returns true (src/pull.ts:252)
-> every member of the team
A new agent lands at the agents/ root (src/resources/agents.ts:339-350) and reaches every member through the !agent.namespace || branch (src/pull.ts:302).
The namespace an author needs for a rule is the project's knowledge namespace. ProjectResourceNamespacesSchema permits that namespace to differ from the skills namespace the flag resolved, so the flag targets the wrong directory even where a rule is placed by hand first.
2. collectClaudemdFiles has no root-level branch
Its only reads are path.join(claudemdDir, ns), once per active namespace (src/pull.ts:1543-1553). A file directly under claudemd/ is never opened.
A member with no role receives every namespace, because a null roleContext falls through to listDirs (src/pull.ts:1537-1540). Assigning a role therefore removes content from that member's CLAUDE.md.
Where the convention holds and where it breaks
| Resource |
Namespaced on pull |
Root ships to everyone |
--role/--project honoured on push |
| rules |
yes (knowledge) |
yes (src/pull.ts:252) |
no |
| agents |
yes (agents) |
yes (src/pull.ts:302) |
no |
| claudemd |
yes (knowledge) |
no |
not pushable |
skills is the only type the flag places, and only for an item that is new and carries no namespace already. pushCore iterates ['skills','rules','env','agents'] (src/push.ts:512-519), so claudemd has no push path at all.
Reproduction
Rules and agents ignoring the flag:
- Declare a project
front-app in manifest/projects.yaml with a knowledge namespace and a skills namespace.
- In the code repository, run
teamai projects set front-app. The working directory is now bound to that project in project scope.
- Create a rule at
.claude/rules/my-rule.md inside that repository. Project scope resolves tool directories against the project root, not against $HOME (src/types.ts:1521-1532).
- Run
teamai push --project front-app --all.
- Open the resulting MR. The rule is at
rules/my-rule.md, not under the project's knowledge namespace.
Root-level claudemd not shipping:
- Add
claudemd/shared.md to the team repo, directly under claudemd/.
- As a member with a role, run
teamai pull --force.
- Read the
TEAMAI_CLAUDEMD_START block in the tool's CLAUDE.md. It does not contain the content of shared.md.
- Clear the member's role and repeat step 2. The content appears.
Expected behavior
teamai push --project <id> places a rule under the project's knowledge namespace and an agent under its agents namespace. Where a type has no namespace to resolve for the named project, the command fails and names that type, rather than writing to the shared root.
collectClaudemdFiles ships claudemd/*.md to every member, matching the root-level branch that filterRulesByKnowledgeNamespaces already implements.
Suggested fix
Extend the push filter to the two types that already namespace on pull, and resolve each destination from its own axis:
selectedItems.filter(i =>
- i.type === 'skills' && i.status === 'new' && !i.namespace
+ (i.type === 'skills' || i.type === 'rules' || i.type === 'agents')
+ && i.status === 'new' && !i.namespace
)
skills resolves from resources.skills, rules from resources.knowledge, and agents from resources.agents. Where the named project declares no namespace for a selected type, fail and name that type rather than writing to the shared root.
Give collectClaudemdFiles the root branch that filterRulesByKnowledgeNamespaces already has:
collectClaudemdFiles(claudemdDir, roleContext)
for ns of activeNamespaces.knowledge
read path.join(claudemdDir, ns)
+ read the .md files directly under claudemdDir
Environment
Both findings are read from source at main (97a0277, package version 0.22.0). The installed CLI is 0.24.0, the current npm latest.
- OS: macOS 26.5.2
- Node.js: v22.22.2
- teamai: 0.24.0
- Provider: GitHub
- AI tool(s): Claude Code
Logs
No --verbose output applies. Both findings come from reading source at the commit above, not from a captured run.
Description
Three code paths implement the namespace convention differently.
filterRulesByKnowledgeNamespacesis the reference implementation: it matches the first path segment against the active namespace set, and ships any name without a segment to every member.Two paths diverge from it. Neither emits a message when it does.
1.
pushdoes not apply--roleor--projectto rules or agents--projectresolves to the project'sskillsnamespace and collapses intooptions.role(src/push.ts:292-320). Step 4 then applies that namespace to skills alone:A rule pushed with
--project front-appis written torules/<item.name>.md(src/resources/rules.ts:154-155). That path carries no first segment, so the nextteamai pullships the rule to every member of the team.A new agent lands at the
agents/root (src/resources/agents.ts:339-350) and reaches every member through the!agent.namespace ||branch (src/pull.ts:302).The namespace an author needs for a rule is the project's
knowledgenamespace.ProjectResourceNamespacesSchemapermits that namespace to differ from theskillsnamespace the flag resolved, so the flag targets the wrong directory even where a rule is placed by hand first.2.
collectClaudemdFileshas no root-level branchIts only reads are
path.join(claudemdDir, ns), once per active namespace (src/pull.ts:1543-1553). A file directly underclaudemd/is never opened.A member with no role receives every namespace, because a null
roleContextfalls through tolistDirs(src/pull.ts:1537-1540). Assigning a role therefore removes content from that member'sCLAUDE.md.Where the convention holds and where it breaks
--role/--projecthonoured on pushknowledge)src/pull.ts:252)agents)src/pull.ts:302)knowledge)skillsis the only type the flag places, and only for an item that is new and carries no namespace already.pushCoreiterates['skills','rules','env','agents'](src/push.ts:512-519), soclaudemdhas no push path at all.Reproduction
Rules and agents ignoring the flag:
front-appinmanifest/projects.yamlwith aknowledgenamespace and askillsnamespace.teamai projects set front-app. The working directory is now bound to that project in project scope..claude/rules/my-rule.mdinside that repository. Project scope resolves tool directories against the project root, not against$HOME(src/types.ts:1521-1532).teamai push --project front-app --all.rules/my-rule.md, not under the project'sknowledgenamespace.Root-level
claudemdnot shipping:claudemd/shared.mdto the team repo, directly underclaudemd/.teamai pull --force.TEAMAI_CLAUDEMD_STARTblock in the tool'sCLAUDE.md. It does not contain the content ofshared.md.Expected behavior
teamai push --project <id>places a rule under the project'sknowledgenamespace and an agent under itsagentsnamespace. Where a type has no namespace to resolve for the named project, the command fails and names that type, rather than writing to the shared root.collectClaudemdFilesshipsclaudemd/*.mdto every member, matching the root-level branch thatfilterRulesByKnowledgeNamespacesalready implements.Suggested fix
Extend the push filter to the two types that already namespace on pull, and resolve each destination from its own axis:
skillsresolves fromresources.skills,rulesfromresources.knowledge, andagentsfromresources.agents. Where the named project declares no namespace for a selected type, fail and name that type rather than writing to the shared root.Give
collectClaudemdFilesthe root branch thatfilterRulesByKnowledgeNamespacesalready has:collectClaudemdFiles(claudemdDir, roleContext) for ns of activeNamespaces.knowledge read path.join(claudemdDir, ns) + read the .md files directly under claudemdDirEnvironment
Both findings are read from source at
main(97a0277, package version 0.22.0). The installed CLI is 0.24.0, the current npmlatest.Logs
No
--verboseoutput applies. Both findings come from reading source at the commit above, not from a captured run.