Repository navigation
feat(projects): add write commands to manage the projects manifest (add/update/remove) #756
Description
Background
manifest/projects.yaml is the single source of truth for a team's projects (the dimension orthogonal to role, introduced in #375). Today the CLI can only read it:
teamai projects list/set <ids>/members <id>— all read-only or local-config writes.- The manifest writer
saveProjectsManifest()already exists (src/projects.ts:122), but no command calls it — onlysrc/__tests__/projects.test.tsexercises it.
So creating or editing a project means an admin hand-edits manifest/projects.yaml, commits, and opens a PR manually. This is error-prone (a project id becomes a directory name skills/<id>/, learnings/<id>/, so a stray / or .. breaks distribution) and asymmetric with roles, which already has a full admin CRUD surface (roles init/add/update/remove, src/roles-cmd.ts).
Proposal
Add three admin write commands under teamai projects, mirroring roles:
teamai projects add <id> \
--namespaces common,<id> \ # required; applied to knowledge/skills/learnings/agents
--name "<display name>" \ # optional
--description "<text>" # optional
teamai projects update <id> \
--add-namespaces <a,b> \ # union into existing
--remove-namespaces <c> \ # subtract; cannot empty the set
--name "<...>" --description "<...>"
teamai projects remove <id>list/set/membersstay exactly as they are.- No separate
init.projects.yamlis optional by design (a team without partitioning has none, and an emptyprojects: []is valid —src/projects.ts:45-50). The firstprojects addin a repo with no manifest simply creates the file with one entry. This keeps the surface minimal (Occam) while still covering the "start from nothing" path thatrolesneeds a dedicatedinitfor. - Every mutation runs pull → write → push branch → PR (reuse
pushManifestChange, scoped tomanifest/) and supports--dry-run, exactly likeroles.
Design notes (implementation is largely a mirror of roles)
- Reuse existing plumbing.
runRolesEdit(self-mode knowledge-worktree handling for single-repo mode, feat: 让项目级 .teamai/ 跟着 git 仓库走(clone 即完成项目初始化) #198) andpushManifestChangeinsrc/roles-cmd.tsare already generic — lift/share them for aprojects-cmd.tswriter path so self mode is handled identically. - Building blocks already present in
src/projects.ts:saveProjectsManifest(re-validates before writing),findProject,listProjectIds,unknownProjectMessage, and theisSafeNamespaceSegmentguard that rejects unsafe ids at the manifest boundary. - Namespaces.
--namespacessets one namespace set applied across all project resource types (knowledge/skills/learnings/agents), matching how admins actually author them (see Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375 examples where every axis =[<id>]). Per-axis editing stays a hand-edit for now. - Guardrails to mirror from roles: duplicate-id on
add, unknown-id onupdate/remove, "cannot remove all namespaces", and aremovewarning that members with that project active will fall back to shared learnings + role-only sync on next pull.
Local resource reclamation (the important part)
This is where the write commands meet the existing pull-cleanup machinery, and it needs to be called out explicitly:
-
Already works.
resolveResourceNamespaces(src/resource-namespaces.ts:60-88) collects every project's skill namespace from the current manifest (allProjectSkillNamespaces); the active set comes only from the projects this directory selected. A skill in a namespace selected by neither an active role nor an active project lands ininactiveSkillNamesand is deleted by pull Step 3'scleanupInactiveNamespaceSkills(src/pull.ts:1209); agents go throughcleanupInactiveNamespaces.projects set(including clearing) setsstate.lastPullRev = nullto force a full sync + cleanup next pull. So a member switching a directory off a project is already covered. -
⚠️ Gap to fix.allProjectSkillNamespacesis collected from the current manifest. After an admin runsprojects remove <id>, that project and its namespace disappear from the manifest, so the already-deployed skills may fall into neither the active set nor the "known namespace" set — and thus may not land ininactiveSkillNames, so they leak (Step 3b'sknownRepoSkillNames.has(dir)likewise depends on the team repo still knowing the skill, andremoveusually deletes the source dir too). This matchesroles remove's current behavior — it only warns that members fall back, and does not guarantee the old role's skills are cleaned up. -
Fix direction. Verify the leak first; if confirmed, reuse the existing tombstone mechanism (
cleanupTombstonedResources,src/pull.ts:633), or have pull actively prune "projects still active in local config but no longer defined in the current manifest". The data-safety guard staysskillSafeToRemove(src/pull.ts:463): a target with local edits, unpushed files, or a nested.gitis kept + warned, never silently deleted.
Scope / non-goals
- Not reintroducing
projects join/leave. Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375 deliberately rejected member-side join/leave (project membership follows cwd +teamai init --project). This proposal is strictly the admin-side manifest CRUD that Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375 did not cover. - Does not change member activation (
set), pull/push resolution, or namespace semantics — it only closes the cleanup loop for the remove case.
Relationship to other issues
- Builds directly on Proposal: 一个团队仓库承载多个项目(project 作为与 role 正交的维度) #375 (projects as an orthogonal dimension).
- Coordinates with feat(projects): extend project isolation to knowledge/docs/agents/env/hooks/mcp #707 (extending project isolation to more resource types): if the manifest schema grows
docs/env/hooks/mcp, both the--namespacesfan-out and the cleanup logic above should extend to the new axes. No conflict — this issue adds the write surface + reclamation loop; feat(projects): extend project isolation to knowledge/docs/agents/env/hooks/mcp #707 widens what gets written.
Acceptance criteria
add/update/removecreate/modifymanifest/projects.yamland open a PR, in both git and self (single-repo) modes.- Invalid ids (
/,..), duplicate add, unknown update/remove, and namespace-emptying are rejected with actionable English messages. --dry-runprints the intended change without writing/pushing.- Namespace add/update/remove. After
updateadds a namespace, pull delivers that namespace's skills/rules to directories bound to the project; after--remove-namespacesor a rename, pull cleans up the now-inactive namespace's local leftovers (reusingcleanupInactiveNamespaceSkills/resolveResourceNamespaces). Add unit + e2e for "add namespace → pull delivers" and "remove/rename namespace → pull cleans up". - Reclamation after
projects remove(needs verification, likely needs implementation). After an admin runsprojects remove <id>, a member directory that previously ranprojects set <id>must, on the next pull, have the project's exclusive-namespace skills/rules/agents removed with no leftovers; the data-safety guard staysskillSafeToRemove(keep + warn on local changes/unpushed files). Add unit + e2e for "remove → pull → reclaim". - Unit tests cover each command + each guardrail; e2e covers both an add→pull→activate round and a remove→pull→reclaim round.
- Docs updated:
docs/usage-guide.md/.zh-CN.mdmulti-project section, andREADME*if the command table lists project subcommands.
Activity
huiq777 commentedon Sep 24, 2026
I'd like to take this one, if nobody else is on it.
My plan follows the proposal:
projects add/update/removeinprojects-cmd.ts, mirroringroles. I'll reuse therunRolesEdit/pushManifestChangeplumbing by lifting it into a shared helper instead of copying it.addcreatesmanifest/projects.yaml, so there is no separateinit.--dry-run, andremovewarns about members who still have the project active.remove, I'll write the "remove → pull" e2e first to confirm whether the leak is real, both whenskills/<id>/is kept and when it is deleted. If it leaks, I'll fix it in pull, keepingskillSafeToRemoveas the guard.usage-guideen/zh-CN, theskill-datacommand reference) go in the same PR.feat(projects): add admin commands to manage the projects manifest (#756