Skip to content

[feat] keep the resources pull delivers in project scope out of git #915

Description

@SaulMoro

Status as of 2026-10-08

Implementation is published in draft PR #1000 at 6ff1a3fb, stacked on #995. All 17 unconditional slices (1–16 and 19), including their cleanup fixes, are complete. The PR is mergeable. It stays draft until #995 merges, CI passes, and the required live tool checks are verified. This issue remains open.

Implemented:

  • The shared git-exclude module preserves member lines and refuses to overwrite an unreadable file. The flag, delivery records, and union of live worktrees protect owned files while keeping member files visible and addable. The first pull after upgrading from npm 0.26.0 performs a full sync with the flag both on and off.
  • doctor, pull --dry-run, failure notices, credential protection, self mode, HTTP delivery, and uninstall cover the new blocks. Generated workspace cache records are protected by exact paths.
  • Copilot instructions, Codex hook dispatch, OpenCode V2 delivery, Claude and CodeBuddy local MCP scopes, and the docs mirror use their supported owned destinations.
  • Project uninstall keeps and names edited recorded MCP servers. In a workspace without project config, teamai uninstall --force reads the current workspace's local MCP records, including when user config exists. Invalid tool JSON preserves the records and exits 1; repair and retry remove the managed server and plaintext token. Other unconfigured workspaces are not scanned.

Validation at the published runtime code:

  • Build, typecheck, and lint pass. The full unit run had 8,314 passes, 4 fixture failures, and 19 skips. Corrected recorded hashes and the logger mock leave assertions unchanged; both affected files pass all 242 tests on rerun.
  • The single full parallel npm run test:e2e -- --retry=0 had 981 passes, 2 outdated exact-list failures, and 27 skips. It also reported a Vitest onTaskUpdate RPC timeout and exited 1. After including the newly protected .teamai/.gitignore in the expected lists, the affected file passes all 7 tests; removal assertions are unchanged. The full suite was not repeated.
  • The 10 MCP cleanup cases and 7 HTTP delivery cases pass, including in the full parallel run. CI on 6ff1a3fb is in progress and must confirm the final head. The automatic PR review check was skipped on this push.

Before merge, verify that Cursor loads the excluded nested rule, Copilot applies the owned instructions without an attachment, Codex runs context and blocking hooks through the dispatcher, and OpenCode V2 loads context and a project-only MCP server. Conditional slices 17 and 18 remain deferred: claude-internal and tclaude must prove the owned rule file loads; Qoder and Qoder CN must prove settings.local.json supplies MCP.

Accepted deviations and remaining limits are in the PR description. Skills are listed per file. In self mode, Claude retains per-checkout .mcp.json because a shared local-scope key cannot represent branch-specific servers. source remove-http keeps MCP records for a later uninstall in the affected workspace.

The base #995 is open and mergeable at d4a7e416. All executed checks pass; the full GitHub-provider E2E job is skipped. It still requires maintainer approval. Current main is ec7d4db0, already included in #1000.

Related PR Relationship to #915
#1000 Published implementation; draft stacked on #995; head 6ff1a3fb; CI and live tool checks pending
#995 Required ownership, checkout-liveness, delivery, and machine-state base; open, checks pass, approval required
#949 Merged: read-only hooks inject --dry-run; already included through main
#989 Merged: CodeBuddy Windows hooks rendered in POSIX; already included through main
#998 Merged: dispatch timing logs; already included and combined with Codex dispatch
#1001 Merged: parallel E2E runner; included and exercised by the full run above
#780 Open, conflicts with main; named HTTP providers overlap delivery and cleanup files
#923 Closed without merge; no pending change from this PR

The #995 upgrade, HTTP copy-ownership, and HTTP local-scope MCP gaps are implemented in slices 2, 7, and 19. Lock-creation errors reported as busy remain separate in #999.

Terms

  • delivered copy: a file or directory teamai wrote in a project and still owns.
  • kept copy: a copy teamai no longer delivers, or did not overwrite, because the member changed it. It is the member's.
  • foreign file: a file at a path teamai would deliver to that teamai does not own (no delivery record, and no team version matches it; see [bug] Delivery and local-state bugs on main found while scoping #915 #993 bug 12), or, across worktrees, a path that exists in a checkout but is not in that checkout's recorded list.
  • teamai-only file: an untracked shared file whose parsed content holds only entries teamai's per-entry manifest owns, with no other top-level key.
  • git exclude block: a marked block in a repository's info/exclude, with one owner. Say "git exclude" in help, doctor and docs, never bare "exclude", which already names the skill and tool opt-outs (teamai skill exclude, disabledAgents).

Problem

In project scope, pull, init and the HTTP local agent write team resources into the business repo's working tree, and nothing keeps git away from them. Observed with 0.22.0 (git status --porcelain -uall after init, pull and a session start, roles, projects, sources, recall and six agents enabled):

<business repo>/                                   git status
├── .claude/skills/<name>/                         ??  team, role and source skills; the CLI's `teamai` skill
├── .claude/agents/<file>, teamai-recall.md        ??
├── .claude/rules/<rule>.md, <ns>/<rule>.md        ??  selected by the member's roles and projects; teamai-recall.md
├── .claude/rules/teamai-context.md                ??  this member's culture, claudemd/ and recall blocks
├── .claude/settings.local.json                    ??  this member's team hooks (main checkout)
├── .agents/skills/<name>/                         ??  Codex, when that directory exists
├── .codex/skills/<name>/, .codex/agents/<file>    ??
├── .codex/hooks.json                              ??  team hooks
├── .github/instructions/**, skills/, agents/      ??  Copilot
├── .github/hooks/teamai.json                      ??  Copilot built-in and team hooks
├── .github/copilot-instructions.md                ??/M  teamai blocks next to the team's text
├── .kiro/steering/<ns>.<rule>.md                  ??  flattened namespaces (also OMP)
├── .opencode/opencode.json, opencode.json         ??/M  instructions entries; MCP servers
├── .codebuddy/models.json, .codebuddy/.gitignore  !!/??  local agent: model API keys, hidden by a new committable .gitignore
└── .teamai/docs/                                  ??  docs mirror

One git add -A commits all of it. The member's role selection then reaches every teammate, the committed copy drifts from the team repo, and in the models.json case an API key is one git rm --cached .codebuddy/.gitignore away from a commit. Doing it by hand doesn't scale:

  • .gitignore is committed and shared by the whole team. It would have to list the union of every member's tools, roles and projects, and each new delivery path adds lines someone must edit in by hand.
  • Folder patterns (.claude/) hide what the team commits there on purpose.
  • In self mode it breaks delivery: init . skips ignored hook settings (commitPaths in src/utils/git.ts), and a git add without -f in the knowledge worktree refuses ignored .teamai/ paths.

Writing the paths into the repo's .git/info/exclude keeps them local to the clone and commits nothing. #886 already does that for MCP configs that hold resolved tokens.

Decisions

Base: the #993 fixes

This work is one PR stacked on the head of the PR that fixes #993 bugs 1–10 and 12 (bug 11 is withdrawn). It relies on: live checkouts probed from recorded roots (bug 1), ownership proven without a record by team history (bugs 2 and 12, so writers decline a member's same-name skill, rule, agent, doc or MCP server, and adopt unrecorded MCP servers and hook entries that equal a team render instead of duplicating them), Codex .agents/skills ownership (bug 3), machine state out of the tree (bug 4), info/ creation (bug 5), the OpenCode V2 plugin context hook (bug 6), and source skills through the team-skill seam (bug 8).

The git-exclude module

  • One module owns exclude files. Public interface:
    • sync(owner, paths): replace semantics. The owner's blocks hold exactly these paths, each routed to the exclude file of the repository it lands in; the owner's block is emptied in recorded files that receive none. Used by delivered and local-agent owners.
    • ensure(owner, paths): add-only pre-write gate. Returns per path excluded | pending | tracked | reincluded | notWritable | locked | gitFailed | outsideRepo | writeFailed | refused, so a caller can withhold a secret. writeFailed covers write errors outside the documented permission/path errno set; refused covers line breaks and trailing spaces. Both block writing the secret. The MCP wrapper maps failures to its existing failed result. Used by mcp-exclude and the security owner, which keep their add-now, remove-only-when-proven-clean lifecycle.
    • remove(owner | all): removes an owner's blocks, or every teamai block, from every recorded exclude file.
    • report(owner, expectedPaths): read-only; listed, missing, tracked, re-included and stale paths per exclude file.
  • Owner policies: mcp-exclude preserves its markers, trimmed parsing and already-ignored skip, with its frozen tests unchanged. Explicit exceptions: it also refuses line-break/trailing-space paths and merges complete duplicate blocks while preserving member text. Every other owner, including credentials, lists already-ignored paths too.
  • Read errors from an existing exclude file must not become empty content: surface the failure, preserve member lines and withhold the gated secret. Only an absent file may be created. This shared fix is in slice 2. The module also fixes check-ignore -v source paths to resolve relative to the git toplevel.
  • New markers: # [teamai:<owner>:start] / # [teamai:<owner>:end], owner limited to [a-z0-9/_-] (other characters in a provider name are percent-encoded). report and remove(all) find blocks by the # [teamai: prefix.
  • Each owner records the exclude files that hold its block: delivered in the partition state; local-agent owners in their state home (<stateHome>/git-exclude.json).
  • Line rules: one line per delivered file, skills and docs included (only the files teamai delivered into a skill directory, so a member's file added there stays visible and addable); never /<dir>/*; never a directory inside a rules directory; toplevel-relative and anchored; built from the landed real path and its on-disk spelling; feat(mcp): keep project MCP configs with resolved tokens out of git (#882) #886's escaping kept; NFC only when core.precomposeunicode is true, otherwise the on-disk bytes; a path containing a newline or ending in spaces is refused with a structured error naming it (escaping a trailing space is possible in git, but block lines are parsed trimmed; refused for simplicity). Block lines are parsed stripping only \r.
  • Tracked: a path tracked in a live checkout's index is not listed and is reported for that checkout; in a skill directory, the tracked files are reported and the other delivered files keep their lines. Never untracked automatically.
  • A rule that re-includes a delivered path is reported with check-ignore -v.
  • git calls drop git's repository-location variables. The exclude-file lock is shared by every owner and process; a busy lock writes nothing and returns locked.
  • Dry run and report write nothing to exclude files or info/ and create no lock file.

What the delivered block lists

  • Recorder. Every writer reports to one recorder carried through the pull the paths it wrote or confirmed as its own in this run. Writers: resource handlers (skills, rules, agents, docs), the built-in skill, rule and agent deployers, source skills, instruction targets, hook reconciles. Declined destinations (a member's file, a kept copy, a failed write) are never reported. In a dry run, every writer's dry-run branch reports the paths it would write.
  • Source writers report the paths actually delivered, rather than every source-manifest entry. A changed legacy copy may remain in that manifest to keep push from offering it, but it is the member's and stays visible to git. Source destinations in HOME use their recorded absolute paths.
  • When a full sync sees no configured sources, the source writer succeeds with an empty delivered set and drops its previous exclusion entries. Existing copies the base CLI leaves on disk stay visible and addable; [feat] keep the resources pull delivers in project scope out of git #915 adds no source-file deletion. An actual source failure instead retains previous entries that still exist.
  • Storage. The list lives in the checkout's lastPullByWorkspace record as gitExcludePaths (absolute landed real paths, which may lie outside the checkout, such as <main>/.claude/settings.local.json written from a linked worktree). awaitingFullSync and pruning carry it. A record without the field (first run after upgrade, or a state file saved by an older CLI) is treated as awaiting a full sync, so the next pull runs one. Fast-path writers add what they report (add-only); only a full sync replaces the list. In a full sync where a writer failed, that writer's previous entries whose paths still exist are kept.
  • When sync runs. On every non-dry pull, fast path included, inside the partition sync lock, after pullSources, skipped for a contended scope (the holder syncs every checkout's list). HTTP scopes take no sync lock; their delivered set is the same in every checkout. Normal git-mode init is covered by its trailing pull. Self-mode initSelfRepo syncs after its own writers finish, including .github/hooks/teamai.json, because it has no trailing pull (slice 10). Also sync after uninstall --agent. The post-source update changes gitExcludePaths on the latest checkout record without losing earlier passes' fields.
  • Union. For an exclude file, the block is the union of the lists of live checkouts (liveness from [bug] Delivery and local-state bugs on main found while scoping #915 #993 bug 1) whose paths route to it.
  • Foreign in another checkout. A path is foreign in checkout X when it exists in X (lstat) and is not in X's list. A live checkout without a list contributes no foreign files until its first full sync. A delivered path that is foreign in any live checkout gets no line, and pull names it with the reason (doctor: below).
  • .claude/settings.local.json is listed while teamai's hook manifest has entries in the main checkout's file, whatever else it holds; it is never foreign in other checkouts (Claude Code treats it as personal).
  • Kept copies (the member changed them) are not reported, so they leave the block and show in git status, for every resource type.
  • Every project session start creates the session tool's root, so a checkout's list grows with each tool the member opens there. The local agent's claudemd installs stay in its cache; nothing is listed for them.
  • Refused paths and teamai-only exits (below) are recorded like sync failures and shown on the next interactive pull.

Flag

  • Team setting sharing.gitExclude.enabled, default false. init writes true whenever it writes a new teamai.yaml in git mode, self mode included; never into the HTTP stub; joining or re-initializing an existing team never rewrites it.
  • Member override gitExcludeEnabled in the partition config.yaml, hand-edited, resolved before the team value, like recallEnabled. No new command.
  • Legacy, un-migrated layouts with a per-worktree config: the flag is read from the partition config only; a legacy checkout contributes its list but never decides the flag; doctor names the un-migrated layout.
  • Local-agent owners follow the gitExcludeEnabled of the config that owns their state home: ~/.teamai/config.yaml for user-scope HTTP, the partition config for project-scope HTTP.
  • With the resolved flag off, a git-mode pull removes only the delivered block; it never removes another owner's block. The security and mcp-exclude owners ignore the flag.

Security owner

  • Before the local agent writes .codebuddy/models.json in project scope it calls ensure through the security owner. On tracked, reincluded, notWritable or locked, the key is not written and the member is told why and how to fix it, as feat(mcp): keep project MCP configs with resolved tokens out of git (#882) #886 does for MCP.
  • teamai stops creating .codebuddy/.gitignore, and removes the one it created when it holds only teamai's two lines (# Local model credentials, models.json).
  • Its line is removed only after models.json is deleted; otherwise uninstall keeps it and warns.

teamai-only files

  • A file is teamai-only when it is untracked and its parsed content holds only entries teamai's per-entry manifest owns, with no other top-level key ($schema counts as foreign). Entry per type:
    • MCP configs without a resolved value (.cursor/mcp.json, .github/mcp.json, .codex/config.toml, Kiro, OMP, Pi, .workbuddy/mcp.json, root opencode.json on OpenCode V1): server entries under the MCP key;
    • .codex/hooks.json: hook handlers in teamai's main-checkout hook manifest;
    • .opencode/opencode.json on OpenCode V1: teamai's instructions entries.
  • The decision runs after the base PR's per-entry adoption, so entries teamai wrote before an upgrade or before its manifest was lost count as teamai's, and such a file stays in the project, excluded.
  • A teamai-only file is listed; the first run that sees a foreign entry removes its line and records the notice <path> now holds entries teamai does not own, so git can see it.
  • Documented limit: when a teammate commits the same path and the member runs git pull, git silently overwrites the excluded copy; teamai's entries return on the next pull, merged into the now-tracked file; entries the member added after the last teamai run are lost.

Relocations

  • Copilot. All of teamai's blocks move to a teamai-owned .github/instructions/teamai-context.instructions.md with applyTo: "**" (always included, a question with no file included, in Copilot CLI and VS Code chat; read in source). The retire path strips the blocks from copilot-instructions.md and deletes it only when teamai created it. Documented: VS Code and Visual Studio code review and Eclipse chat do not read .github/instructions; VS Code needs chat.includeApplyingInstructions (default on). Before merge, record a live Copilot check.
  • Codex hooks. A teamai-only .codex/hooks.json is listed. When the project's hooks.json holds entries teamai does not own (team-owned, or tracked), teamai installs one dispatcher entry per event that has team hooks in such a project in ~/.codex/hooks.json (teamai hook-dispatch team-hooks --event <E> --tool codex, timeout = the largest team-hook timeout for that event), separate from the built-in entries. The dispatcher resolves the project from the hook's cwd, evaluates matchers, applies per-hook timeouts, passes through exit code 2 and stdout JSON, and exits at once when the project has none. On the run that sees a teamai-only file become team-owned, teamai removes its entries (recorded or adopted) from the file through the manifest and installs the dispatcher entries, so no team hook runs from both places; the reverse when the file is gone. Trust is set for the fixed entries. Before merge, record a live Codex check.
  • OpenCode. Major version detected with opencode --version at pull time, cached per run; absent or unparsable means V1.
    • V2: context and rules arrive through the plugin ([bug] Delivery and local-state bugs on main found while scoping #915 #993 bug 6). Team MCP servers go through the plugin's MCP transform for the current project. teamai removes its own V1 artifacts (recorded or adopted) from untracked teamai-only files: its instructions entries from .opencode/opencode.json and its servers from the root opencode.json (V2 also reads that file, so they would duplicate); a tracked or mixed file is left and named by doctor. A later return to V1 puts them back.
    • V1: .opencode/opencode.json (instructions) and the root opencode.json (MCP) stay, under the teamai-only rule. (Moving V1 MCP to .opencode/opencode.json was rejected: it would still be a file the team may track, for the cost of a migration.)
    • The plugin reads project MCP from a teamai-owned .opencode/teamai-mcp.json, included in its walk-up marker set and git-exclude policy. Check readiness after reconciling the plugin in the same pull, so an older plugin is updated before removing V1 artifacts. If the plugin remains unavailable or its write fails, keep the V1 artifacts and have doctor name the reason.
    • Before merge, record a live V2 check.
  • Claude MCP moves to Claude's local scope: ~/.claude.json → projects[<realpath of the main checkout>].mcpServers, shared by every worktree, no approval prompt. Ownership becomes main-checkout-wide; migration removes teamai's entries (recorded or adopted) from .mcp.json through the MCP manifest, keeping names CodeBuddy still claims until its move.
  • CodeBuddy MCP moves to ${CODEBUDDY_CONFIG_DIR:-~}/.codebuddy.json → projects[<realpath of each worktree root>].mcpServers, written by pull and by the worktree preparation; ownership per worktree; the next pull drops teamai's keys for worktrees that are no longer live. teamai then writes no .mcp.json; Copilot keeps its own .github/mcp.json. docs/designs/team-secrets.md (both languages) is updated.
  • WorkBuddy MCP stays in <project>/.workbuddy/mcp.json under the teamai-only rule (its app reads that file; confirmed 2026-10-07).
  • Local agent MCP in HTTP mode. Slice 19 gives install_mcp and uninstall_mcp the same Claude and CodeBuddy local-scope targets. The local-agent manifest records those entries; its next sync moves recorded servers from .mcp.json or mcp.json, preserving the member's entries and entries another tool owns.
  • Conditional, needing a human with the tool: claude-internal and tclaude context to an owned .<variant>/rules/teamai-context.md; Qoder MCP to .qoder/settings.local.json under the teamai-only rule (not owned whole). Without the live check, they stay visible.
  • .codex/config.toml, .cursor/mcp.json, .github/mcp.json, Kiro, OMP and Pi MCP have no per-member location outside the repo; when the team tracks them, teamai's entries stay visible.

Self mode

  • .teamai/ is never excluded and no .teamai/.ignore is written. Built-in hooks stay in the committed tool settings. Team hooks move out of tracked settings: Claude to .claude/settings.local.json; Codex by the Codex rule (the committed hooks.json is team-owned, so the dispatcher). Copilot's hook file is listed. .cursor/hooks.json and .codebuddy/settings.json stay visible.

Docs

  • Only when resolveDocsDestination resolves to <root>/.teamai/docs (non-self projects): teamai lists one line per delivered docs file, never the whole directory, so a member's own file or a kept edited doc there stays visible and addable. It writes its whitelist inside # [teamai:delivered:start] / # [teamai:delivered:end] markers in .teamai/.ignore, as exactly !/docs/**. The .ignore file is listed only when it holds nothing but teamai's block. Flag off, uninstall and a switch to self mode remove the block and delete the file when it is then empty. If the member's own ignore rules exclude .teamai/ as a whole, ripgrep never reads that file, and the docs are reachable only by exact path.
  • Any other docs location gets one line per delivered docs file, no .ignore.
  • The help text of an existing command names the resolved docs path; the generated commands reference is regenerated; the core skill gains a line only if no command's help fits.

Commands

  • pull --dry-run prints one line per block and writes nothing to exclude files or info/, and creates no lock: [dry-run] Would list N path(s) and drop M in teamai's delivered git exclude block in <file>, or [dry-run] Would remove teamai's delivered git exclude block from <file> (sharing.gitExclude is off).
  • uninstall takes the partition sync lock, deletes files first, then calls remove(all), except that mcp-exclude and security lines for files that still hold a credential keep feat(mcp): keep project MCP configs with resolved tokens out of git (#882) #886's proven-clean rule and its warning. uninstall --agent <tool> removes, from every checkout's persisted list, the paths under that tool's resolved roots (scopedToolPaths, plus .agents/skills for Codex), persists the filtered lists, and re-syncs. uninstall --dry-run shows Git exclude blocks (teamai's): per owner and file, replacing the MCP-only header. A read-only exclude file produces a warning with the lines to delete by hand; a deleted repository is skipped.
  • doctor checks, next to buildMcpGitExcludeCheck, with git calls batched (one git ls-files -z --others --exclude-standard -- <paths> per exclude file, then check-ignore -v only for paths that are not ignored):
    • flag off, doctor stage only, informational: Delivered team resources are visible to git: N untracked (first 5: …), naming both switches (sharing.gitExclude.enabled: true in teamai.yaml, or gitExcludeEnabled: true in );
    • flag on, failing (also after interactive pulls): a delivered path not listed or not ignored, naming any re-including rule; a damaged block; a foreign path in another checkout; the last silent failure;
    • flag on, informational: delivered paths git tracks, with git rm -r --cached <path>; stale lines;
    • the flag's source (team, member config, default); an un-migrated layout.
  • Silent pulls record the last sync failure per partition (the git-hook failure record pattern), shown on the next interactive pull and by doctor, cleared by the next success.

Path table

Paths teamai writes in project scope, after all slices.

Path Kind Excluded
.<tool>/skills/<name>/ (team, role, project and source skills; the CLI's teamai skill), .agents/skills/<name>/ (Codex), .github/skills/<name>/ one line per delivered file yes
agents: .<tool>/agents/<file>, .github/agents/<name>.agent.md, teamai-recall agents whole file yes
rules: every tool's rules directory, recursively, one line per file, including namespace subdirectories, Kiro and OMP flattened names, .github/instructions/**/*.instructions.md, teamai-recall rules whole file yes
teamai-context files: .claude/rules/teamai-context.md, .cursor/rules/teamai-context.mdc, .codebuddy/rules/teamai-context.md, .opencode/teamai-context.md, .github/instructions/teamai-context.instructions.md whole file, the member's selection yes
.claude/settings.local.json listed while teamai has entries in the main checkout's file yes
.github/hooks/teamai.json whole file yes
.opencode/teamai-mcp.json (V2) whole file read by the plugin flag on; always through mcp-exclude when it holds a resolved value
.codex/hooks.json, MCP configs without a resolved value, .opencode/opencode.json (V1), root opencode.json (V1), .workbuddy/mcp.json teamai-only file while teamai-only
MCP configs with a resolved value mcp-exclude (#886) yes, always
.codebuddy/models.json (local agent) credential yes, always; not written if it cannot be
skills and rules the local agent installs in project scope whole item yes, own block
.teamai/docs/, .teamai/.ignore per delivered file; whitelist file yes, non-self, default docs location only
.github/copilot-instructions.md teamai content moved out nothing left
.mcp.json MCP moved out except Claude in self mode in self mode, only while teamai-only
.claude-internal/CLAUDE.md, .tclaude/CLAUDE.md, .qoder/settings.json until their live checks no
tracked or team-owned MCP and hook files; .cursor/hooks.json and .codebuddy/settings.json in self mode teamai entries next to the team's no

Out of scope

Slices

One PR stacked on the head of the #993 fix PR, built in this order (each slice one or more commits):

  1. Prefactor: a git-exclude module that owns exclude files (after: none)
  2. Tracer bullet: the flag, the recorder, and handler-delivered skills out of git (after: 1)
  3. Every writer reports: the full delivered set out of git (after: 2)
  4. Worktrees and repository layouts (after: 2)
  5. doctor, pull --dry-run and silent failures (after: 2)
  6. uninstall removes every block it safely can (after: 4, 5)
  7. Local agent deliveries out of git (after: 4, 6)
  8. Credentials: models.json always out of git (after: 2, 7)
  9. teamai-only files (after: 3)
  10. Self mode: team hooks out of tracked settings (after: 3)
  11. Copilot: teamai's blocks in a file teamai owns (after: 3)
  12. Codex: team hooks without writing into the team's hooks.json (after: 9)
  13. OpenCode: V2 through the plugin, V1 under the teamai-only rule (after: 3, 9)
  14. Claude: team MCP servers in Claude's local scope (after: 9)
  15. CodeBuddy: team MCP servers in CodeBuddy's local scope (after: 14)
  16. Docs mirror out of git, still searchable (after: 3)
  17. claude-internal and tclaude: own context files (conditional) (after: 3; needs a human with the tool)
  18. Qoder: MCP in settings.local.json (conditional) (after: 9; needs a human with the tool)
  19. Local agent: project MCP servers in the tools' local scope (after: 7, 15)

Testing

Good tests assert what a member sees through git and the CLI, never internal lists or state files. The property is two-sided: every path teamai delivered is invisible to git status, and every path the member or the team owns stays visible and addable. A test that only checks a clean git status is not enough, because hiding a member file also makes it clean.

Seams (two):

  • The real CLI, built, spawned asynchronously (spawn, not spawnSync, when an in-process mock server runs), in a sandboxed HOME with global git config and excludes isolated (HOME, XDG_CONFIG_HOME, GIT_CONFIG_NOSYSTEM) and local bare repositories reached through url.<path>.insteadOf. Prior art: init-project-all, git-hook-new-worktree, instruction-targets, doctor-delivery-cli, mcp-uninstall. It carries the acceptance property and every lifecycle story. The HTTP case extends the in-process mock backend with apply_model_config, install_rule with rule-file download, scope/workspace_path on every command type, and the routes init --http calls (get-config, projects/mine, plugins/config).
  • The git-exclude module's public interface (sync, ensure, remove, report) against real temporary git repositories, following the MCP exclude tests. It covers routing into submodules and nested clones, separate git dirs, damaged and duplicated markers, member lines and CRLF preserved byte for byte, escaping and refused paths, re-including rules, tracked descendants, the busy lock, dry run creating nothing, and two owners (local-agent, providers/http/x) in one exclude file. Case-insensitive cases are skipped unless the temp directory is case-insensitive (macOS CI runs them).

No other seam. The recorder, liveness and routing are tested through these two.

Acceptance

Acceptance fixture (CLI seam):

  • Team repo with roles, projects, sources, recall on, team hooks, MCP with and without resolved values; Copilot plus at least five other agents; git mode and HTTP mode; an existing .agents/skills/ directory for Codex.
  • Two live checkouts; a foreign file at a delivered path in one of them (a member's rule, which [bug] Delivery and local-state bugs on main found while scoping #915 #993 bug 12 keeps); an unrelated member file; a skill with a tracked SKILL.md and a newly delivered untracked file.
  • After every step, git status --porcelain -uall lists only paths in an explicit allowlist of visible paths: the path table's "no" rows plus the rows a later slice still has to move; each slice removes its rows from the allowlist. The foreign and member files show in git ls-files --others --exclude-standard and are staged by git add -A in the disposable repo; the tracked SKILL.md change is visible and the new file in that skill is not; no line ends in /* or names a directory under a rules directory.
  • Transitions: role switch, uninstall --agent codex, a kept copy, flag off, full uninstall leaving no teamai block, a --separate-git-dir repo with a linked worktree, a tool folder that is a submodule, git stash -u and git clean -fd keeping delivered copies.
  • Batched doctor git calls are asserted with a counting git wrapper on PATH (at most a constant number of calls per exclude file).
  • Per relocation slice: the shared file no longer changes, and the tool still receives the content (its config file, or the plugin seam for OpenCode).
  • Platform contract (native Git for Windows, WSL): not checked in CI (ubuntu and macOS only); documented as not checked.

Docs to update with the change

  • docs/usage-guide.md and docs/usage-guide.zh-CN.md, the instruction files section: "teamai does not change .gitignore, .git/info/exclude or the git index" describes main before [feat] keep the resources pull delivers in project scope out of git #915 and changes with the flag.
  • skill-data/setup/references/manage-admin.md and uninstall.md: the block is no longer MCP-only.
  • A line for agents: excluded skills and rules still load; editor and agent searches over their folders skip them, so open them by path or with teamai skill path.
Do the tools still load excluded files?

Yes, for every tool checked. Exclude patterns must avoid two shapes.

Tool Loads skills, rules, agents Evidence
Claude Code 2.1.287 yes, nested rules included live, per-item and whole-folder excludes
Codex CLI 0.160.0 yes live with per-item excludes; source (read_dir)
OpenCode 1.18.34 yes, instructions globs included live, /.opencode/ excluded; source (npm glob)
Copilot CLI 1.0.90 yes, .github/instructions and .claude live, per-item and whole-folder
Cursor CLI 2026.09.28 yes, with the pattern rule below source and its bundled rg; not live
Cursor IDE, VS Code Copilot, Kiro, CodeBuddy not checked
  • Cursor lists rules with rg started inside .cursor/rules. An excluded subdirectory there (/.cursor/rules/frontend/, /.cursor/rules/*) silently drops the nested rules. Whether Cursor loads an excluded file inside a namespace subdirectory (.cursor/rules/frontend/x.mdc, one line per file) is not verified yet.
  • When an OpenCode skill runs, OpenCode lists its files with rg. /<skill>/ keeps them; /<skill>/* hides them.
  • Copilot (VS Code and CLI) lists .github/instructions without applying git ignore rules.
Git behavior the design relies on (verified with git 2.55)
  • A linked worktree resolves git rev-parse --git-path info/exclude to the common dir; a submodule's exclude file is in .git/modules/<path>/ (per worktree inside a linked worktree).
  • In git mode the teamai project root is always the checkout's toplevel, even when init runs in a subdirectory, and one repository holds at most one teamai project, so one delivered owner per repository is enough.
  • git worktree list --porcelain names the git dir, not the main checkout, for --separate-git-dir repositories and submodules.
  • Excluded copies survive git clean -fd and git stash -u (so the tool roots survive, and delivery keeps working; today git clean -fd deletes .claude/ and .codex/ and the next pull delivers nothing). git clean -fdx and git stash -a remove them, as they remove every ignored file.
  • With core.ignorecase, exclude matching ignores case but ls-files --error-unmatch does not.
  • A file line can be re-included by a member's negation (for example !*.md in a .gitignore); teamai reports it as re-included, naming the rule, and the sync fails until the rule goes.
  • ripgrep skips an excluded item when searching its parent, and finds it when given the item's own path.
Alternatives considered
  • Exclude whole tool folders (.claude/, .codex/). Hides what teams commit there on purpose; a new team file would never be committed, with nothing to warn about it.
  • Write the paths into the committed .gitignore. Changes a file the team owns, on every member's machine.
  • A generated .gitignore inside each delivered skill. * also hides the skill's contents from a search started inside it, and teamai does not own every file in a skill directory.
  • Repo-local core.excludesFile. Replaces the member's global excludes file instead of adding to it.
  • skip-worktree / assume-unchanged. Cannot mark untracked files; on tracked files they hide the member's edits and abort pulls.
  • A local clean filter for mixed files. Works mechanically, but after each teamai write the file shows modified until every worktree's index is refreshed, stash/merge/rebase drop the block, a missing filter command commits the block or breaks git status, it does nothing for untracked files, and libgit2 clients bypass it.
  • Exclude any shared file teamai created alone, without a per-entry manifest. Cannot tell teamai's entries from the member's.
  • Per-worktree exclude configuration. Avoids the clone-wide scope, but needs extensions.worktreeConfig, per-worktree config and its lifecycle; skipping a contested line is enough.
  • On by default for every team. Would silently change what git shows for teams that commit delivered files on purpose.
Verification and what is not checked

Verified on 0.22.0 (f287dcb) in sandboxed homes, with local bare repositories:

  • The observed tree above, including .github/hooks/teamai.json, teamai-recall files, .agents/skills, nested and flattened rules, .codebuddy/models.json and its .gitignore.
  • The local agent writes skills and rules into the project, and uninstalling them through the agent removes both files even when excluded.
  • Delivery records keep a disabled tool's paths after uninstall --agent and pull --force, and keep skills the team deleted; the fast path computes no desired sets.
  • Concurrent pulls in two worktrees serialize on the partition lock.
  • Edits to excluded copies are still offered by push; teamai status counts improve in self mode.
  • Claude local MCP scope (2.1.292), CodeBuddy local scope (2.161.4), OpenCode 2.0.24 mcp.transform, docs search per agent, dispatcher latency.

Not checked:

  • Native Windows and WSL; git versions before 2.55 (the design needs --git-path and -z worktree output, git 2.36).
  • Live model sessions for the Copilot applyTo: "**" file (read in source only; the installed CLI refused the model), the OpenCode V2 context hook, and the Codex dispatcher.
  • JetBrains and Xcode Copilot chat (GitHub's docs disagree), Cursor's semantic index, VS Code search.
  • claude-internal, tclaude and Qoder.
  • Self mode end to end with team hooks moved.
  • It also helps Proposal: a workspace mode for features that span several repos #913, where a workspace can be a git meta repository that receives its children's resources.

Open questions:

  • Should a later major version turn the flag on by default for every team?

Activity

  1. SaulMoro commented on Sep 30, 2026

    @SaulMoro
    CollaboratorAuthor

    Correction to this note, and where the issue stands after #886 and #940 merged, with #952 open and #946 next. The body is updated to match.

  2. SaulMoro commented on Oct 2, 2026

    @SaulMoro
    CollaboratorAuthor

    @jeff-r2026 With #952, #957, #958, #964 and #956 waiting for review, this issue is the piece that closes project scope for a team whose members use different AI tools and roles in the same repo:

    Problem Solved by
    Role blocks collide in the shared CLAUDE.md and AGENTS.md #952, #957: one file per member and tool
    The first AI session (after init, in a new worktree, after a team change) reads its config before the SessionStart pull, so rules, MCP and team hooks are missing or stale #964; #958 for worktree hooks
    git add -A commits a member's selection, team skills or source skills, and the copy drifts from the team repo #915, on top of #886's block

    What it does not cover: .github/copilot-instructions.md and .codex/hooks.json keep the member's selection next to content the team may track, and a path someone has already committed is only reported by doctor. .teamai/docs/ stays visible until agents can search it while it is excluded (below).

    Changes since the 09-30 comment. The body is updated to match:

  3. self-assigned this
    on Oct 4, 2026
  4. 76 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions