Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb4f042
fix(push): namespace new rules and agents from --role/--project (#649)
SaulMoro Sep 21, 2026
5c9b0aa
fix(push): record rule placement in state instead of matching by base…
Sep 22, 2026
099d8ff
fix(push): stop on unreadable roles manifest, sync placed rules, reso…
Sep 22, 2026
0db7219
test(push): cover #649 placement with the real CLI across agents and …
Sep 22, 2026
d81c204
fix(push): keep a placed resource maintainable and removable by its a…
Sep 22, 2026
0afa9dc
fix(push): narrow what a placement record grants, and when it is written
Sep 22, 2026
9d1295b
fix(push): let --role/--project decide which team agent a local edit …
Sep 22, 2026
dcb6291
fix(push): never place a new resource onto one that is already there
Sep 22, 2026
9df2898
fix(push): resolve a placed agent on removal, and collide on either e…
Sep 22, 2026
7a313f1
fix(remove): make the placed-agent resolution actually reach the command
Sep 22, 2026
ccc13ec
fix(push): raise a project agents-axis failure the scan would otherwi…
Sep 22, 2026
feb0533
fix(agents): reach the canonical source, and hold the record to what …
Sep 22, 2026
ec46f9a
fix(agents): deliver recorded agents on pull, and let a named destina…
Sep 22, 2026
9488cd9
fix(pull): stop revoking the agent pull had just delivered
Sep 22, 2026
18ebc34
fix(pull): stop the stale sweep from deleting the author's own placed…
Sep 22, 2026
7034a51
fix(push): defer the agents-axis failure to selection, reload project…
SaulMoro Sep 22, 2026
7979af4
fix(push): stop on a stale clone under --project, retire a renamed ca…
SaulMoro Sep 22, 2026
708238d
fix(push): record a placement only once it has landed, withdraw it wh…
SaulMoro Sep 22, 2026
b9ecf7b
fix(push): consume a pending placement once it is recorded
SaulMoro Sep 22, 2026
d292dae
fix(remove): keep placement records until the removal lands, and reti…
SaulMoro Sep 22, 2026
c48225e
fix(pull): reconcile single-repo records through origin/<default>, an…
SaulMoro Sep 22, 2026
c2a54b3
fix(remove): stop on a stale clone, and judge PR conflicts by the des…
SaulMoro Sep 22, 2026
665cd4b
fix(push): prefer an agent's delivered source over the flag, baseline…
SaulMoro Sep 22, 2026
859ac58
fix(push): close the round-21 findings on placement, removal and agen…
SaulMoro Sep 23, 2026
23717ec
fix(push): stop on unsaved records and stale placements, name namespa…
SaulMoro Sep 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
2 changes: 1 addition & 1 deletion docs/designs/multi-project-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ projects:
agents: [hai-inference] # optional; agents/<namespace>/ scoped to this project
```

Agent push uses the same role/project namespace resolution as pull and skips ambiguous source destinations. On a role or project change, agent cleanup checks each tool destination independently, including YAML `targets` and legacy format support. Locally edited copies are preserved.
Agent push uses the same role/project namespace resolution as pull and skips ambiguous source destinations. Placement follows it: a new agent pushed with `--role`/`--project` lands under `agents/<namespace>/` (the project's `agents` axis), the same way a new rule resolves from `knowledge` and a new skill from `skills` (issue #649). On a role or project change, agent cleanup checks each tool destination independently, including YAML `targets` and legacy format support. Locally edited copies are preserved.

Directory layout reuses the existing namespace convention, adding one learnings layer:

Expand Down
34 changes: 26 additions & 8 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,11 @@ teamai projects members hai-inference # Who is registered on a project
Member registration is a **side-effect of `init`**: running `teamai init --project <id>`
appends `<id>` to your `members/<user>.yaml` roster (append + dedupe across
directories), so the team can answer "who is on project X". `teamai push --project <id>`
pushes skills into that project's skills namespace (resolved from the manifest),
mirroring `teamai push --role`.
pushes each new resource into that project's namespace for its own resource type
(resolved from the manifest): a skill into `resources.skills`, a rule into
`resources.knowledge`, an agent into `resources.agents`. If the project declares
no namespace for a type being pushed, the push stops and names that type rather
than writing to the shared root, where the resource would reach everyone.

Example local config:

Expand Down Expand Up @@ -575,10 +578,10 @@ Exclusion rules take effect after role and tag filtering. When running `teamai p
```bash
teamai push # Scan for new/modified resources, create an MR
teamai push --all # Skip confirmation, push directly
teamai push --role pm # Push this skill to skills/pm/<skill-name>/
teamai push --role pm # Push into the pm namespace (skills/pm/, rules/pm/, agents/pm/)
```

**Namespace selection (new skills):** When pushing a new skill, the CLI automatically detects available namespaces and offers an interactive choice:
**Namespace selection (new resources):** When pushing a new skill, rule or agent, the CLI automatically detects available namespaces and offers an interactive choice:

```
Which namespace should new skills be pushed to?
Expand All @@ -588,10 +591,25 @@ Which namespace should new skills be pushed to?
Choose namespace [1-3] (default: 1 = common):
```

- Each resource type resolves from its own axis: skills from the `skills` namespaces, rules from `knowledge`, agents from `agents`. A push that carries several types asks once per axis
- If `primaryRole` is set, the list of available namespaces is expanded from the manifest
- If `primaryRole` is not set, the team repo's directory structure is scanned automatically
- If `primaryRole` is not set, the team repo's directory structure is scanned automatically for skills; a new rule or agent stays at the shared root
- A single namespace is auto-selected; use `--role <id>` to choose one explicitly
- Modifying an existing skill automatically keeps its original namespace
- Modifying an existing resource automatically keeps its original namespace
- The chosen destination is printed for each resource, e.g. `[rules] my-rule → rules/pm/my-rule.md`
- A roles manifest that exists but cannot answer — unparseable, or missing the configured role — stops the push instead of falling back to the shared root: fix `manifest/roles.yaml`, run `teamai roles set <role>`, or pass `--role <ns>`. A team with no `manifest/roles.yaml` at all keeps the pre-manifest behavior
- `teamai push --dry-run` resolves the same destinations and stops on the same unresolvable namespace, so it never reports a push as viable that the real command refuses
- When several namespaces could take a new resource and there is no terminal to ask on (CI, a hook, `TEAMAI_NONINTERACTIVE`), push stops with exit 2, lists them, and asks for `--role <ns>`
- `--role`/`--project` places new resources only. An edit of a shared-root rule or agent stays at the shared root, and push says so
- A placed resource stays maintainable from the machine that published it. While its PR is open, the open-PR record routes a later edit of the author's own copy back to that PR; once the file is on the default branch, `state.json` records where push put it, so the edit goes back to the same file, and an agent published into a namespace this directory has not activated is still editable rather than skipped as having no active source
- `teamai remove rules <name>` accepts the bare name the author's copy carries as well as the published `<namespace>/<name>`; it reports which one it resolved to, and removes both the namespaced team file and the author's copy at the rules root. If the team repo cannot be refreshed first, or this machine's placement records cannot be updated and saved, `remove` stops with exit 1 and removes nothing, because either can resolve the name to the wrong files
- A local agent is an edit of the team agent it was delivered from: one in an active namespace or at the shared root first, then one this machine placed. Only when neither exists does `--role`/`--project` decide, and the agent is new in that namespace; if that namespace already holds an agent of that name, the agent is skipped rather than written over it, as a rule would be. Two active agents of one name stay ambiguous and are skipped, flag or not. The same agent name may exist in several namespaces, so a copy in an inactive one you did not name never blocks publishing yours. A placed agent that changed on the team since this machine last synced it is held until you run `teamai pull`, because agents have no pre-push sync. In single-repo mode, a root copy under `.teamai/` that matches an older version of the file it was placed at is held too: nothing refreshes it, so it is an old copy rather than an edit
- A new resource is never placed on top of one that is already there. If the resolved namespace already holds that name, the push stops and names the file: pull and edit the existing copy, rename yours, or pick another namespace with `--role <ns>`
- An agent whose namespace is not active here stays editable through its placement record, and `pull` delivers it for the same reason, so your copy tracks the team file. An active namespace holding that name wins: that agent is the one deployed here
- A resource awaiting review in an open PR keeps that PR's destination — unless this push names a namespace other than the one recorded (the shared root counts as one), in which case the flag decides, the open PR is left untouched, and the collision is reported
- If the team repo cannot be refreshed at the start of a push, `--project` stops instead of placing by a possibly stale `manifest/projects.yaml`; so does any new resource placed without `--role`, because its destination comes from that clone (`manifest/roles.yaml`, its absence, or the namespaces the repo already has). Fix the pull and retry, or name the namespace with `--role <ns>`. `push` also stops, and pushes nothing, when this machine's placement records cannot be updated and saved
- A placement record is written only once the pushed file has landed on the default branch, so a PR closed without merging leaves none behind, whatever became of its branch. It is dropped again when the team deletes that file, or when a shared-root file of the same name appears (your root copy then follows that file, and `pull` warns). `push`, `pull` and `remove` settle this before they read the records. `teamai remove` itself leaves the record alone: its deletion reaches the default branch only when its PR merges, and until then a retried `remove` still resolves the bare name to the namespaced file. If the file reached the default branch with content other than what you pushed (for example a reviewer changed the PR before a squash merge), it is not recorded, and push says so once; run `teamai pull` and edit that file as the team file it now is
- Your own copy of a rule you published into a namespace stays at the rules root. When that namespace is active here, `pull` updates that copy instead of writing a second one under `rules/<namespace>/`; when it is not, `pull` leaves it alone. It is swept only once the team file it was placed at is gone

**Updating an open PR instead of duplicating it:** If a resource is already waiting in an unmerged PR, re-running `teamai push` on it updates that existing PR in place (by force-pushing its branch) rather than opening a duplicate. Keep the resource selected to update its PR; deselect it to leave the PR untouched. Unrelated resources selected in the same run go into their own new PR. Once the PR merges (or its branch is removed from the remote), the record is cleared and the next push opens a fresh PR as usual.

Expand Down Expand Up @@ -1479,7 +1497,7 @@ roles:
agents: [common, frontend] # optional; omitted = root-level agents only
```

`teamai pull` copies these into each Tier-1 tool's `agents/` directory (e.g. `~/.claude/agents/`), flattened by file name, so two active namespaces must not define the same agent name (pull reports the collision and skips the scope). `teamai pull` writes `<name>.toml` for Codex tools, `<name>.json` for Kiro, `<name>.agent.md` for Copilot, and `<name>.md` for every other tool. When a member changes role, agents of the namespaces that stopped being active are removed on the next pull, unless the deployed copy was edited locally, in which case it is kept with a warning. Without a configured role, every agent syncs. `teamai push` resolves the source using the same active role and project namespaces as pull. It writes edits to that source and skips ambiguous destinations with a warning; an agent with only inactive sources is also skipped. Skipped agents do not block other resources in the same push. A new agent lands at the root. Cleanup checks each tool separately, respecting YAML `targets` and legacy format support. An active same-named agent protects a deployed file only when it targets that tool and output file. `teamai remove agents <name>` records a tombstone. The next pull on every other machine deletes `<name>.agent.md`, `<name>.md`, `<name>.toml` and `<name>.json` from each synced tool's agents directory. That cleanup also runs when the pull finds the team repo unchanged. The CLI's built-in `teamai-recall` profile is deployed alongside team agents but is not uploaded by `teamai push`.
`teamai pull` copies these into each Tier-1 tool's `agents/` directory (e.g. `~/.claude/agents/`), flattened by file name, so two active namespaces must not define the same agent name (pull reports the collision and skips the scope). `teamai pull` writes `<name>.toml` for Codex tools, `<name>.json` for Kiro, `<name>.agent.md` for Copilot, and `<name>.md` for every other tool. When a member changes role, agents of the namespaces that stopped being active are removed on the next pull, unless the deployed copy was edited locally, in which case it is kept with a warning. Without a configured role, every agent syncs. `teamai push` resolves the source using the same active role and project namespaces as pull. It writes edits to that source and skips ambiguous destinations with a warning; an agent with only inactive sources is also skipped. Skipped agents do not block other resources in the same push. A new agent is placed the way a new skill is: `--role <ns>` or `--project <id>` (that project's `agents` namespace) names the directory, and with neither flag it resolves from the primary role's `agents` namespaces. It only stays at the shared root — where every member receives it — when no namespace resolves, and push warns when that happens (see [Push local resources](#push-local-resources)). Cleanup checks each tool separately, respecting YAML `targets` and legacy format support. An active same-named agent protects a deployed file only when it targets that tool and output file. `teamai remove agents <name>` records a tombstone. A namespaced agent can be named as `<namespace>/<name>`; a bare name that only one namespace has resolves to it, and a bare name found in several places is refused, with the qualified names listed, rather than removed from all of them. The next pull on every other machine deletes `<name>.agent.md`, `<name>.md`, `<name>.toml` and `<name>.json` from each synced tool's agents directory. That cleanup also runs when the pull finds the team repo unchanged. Removing a namespaced agent tombstones `<namespace>/<name>` only, so the same name in another namespace is untouched; a member's flattened `<name>` copy is cleaned, and not pushed again, when it can be that agent's copy (the namespace is active for them, or their machine placed the agent) and their directory does not still receive an agent of that name from another active namespace. A member who never had that namespace keeps their own agent of the same name. The CLI's built-in `teamai-recall` profile is deployed alongside team agents but is not uploaded by `teamai push`.

### GitHub Copilot CLI

Expand Down Expand Up @@ -1900,7 +1918,7 @@ Shared resources (the env block, docs directory, and `~/.teamai/`) are removed *

The exclusion is durable: `uninstall --agent <tool>` drops the tool from `enabledAgents` and records it in `disabledAgents`, so a later `pull` (or another tool's session-start hook) will not resurrect its skills, rules, agents, CLAUDE.md block, or hooks. Running `init --agent <tool>` again clears the exclusion and re-enables sync for that tool.

The same `enabledAgents` whitelist (from `init --agent`) also gates CLI built-in skills/rules/agents and CLAUDE.md-class injects: an already-installed tool outside the list is neither written to nor deleted from, even if its root directory already exists. `teamai remove` respects the same whitelist for agents, rules, and skills, and `teamai pull` / `teamai mcp inject` respect it for MCP servers. Editing `enabledAgents` without `init` still invalidates the last-pull skip cache for newly added tools.
The same `enabledAgents` whitelist (from `init --agent`) also gates CLI built-in skills/rules/agents and CLAUDE.md-class injects: an already-installed tool outside the list is neither written to nor deleted from, even if its root directory already exists. `teamai remove` respects the same whitelist for agents, rules, and skills, `teamai push` reads no rules or agents from a tool outside it, and `teamai pull` / `teamai mcp inject` respect it for MCP servers. Editing `enabledAgents` without `init` still invalidates the last-pull skip cache for newly added tools.

To rejoin after uninstalling:

Expand Down
Loading
Loading