Skip to content

[bug] Delivery and local-state bugs on main found while scoping #915 #993

Description

@SaulMoro

These bugs on main (f287dcb) came up while scoping #915. They do not depend on #915 and can be fixed in parallel with it. Five of them touch code #915 builds on (last column), so fixing those first saves a rebase. The #915 work is a PR stacked on the head of the PR that fixes these. Bug 11 was withdrawn after a check of the WorkBuddy app (see its section).

Each was reproduced with the built CLI 0.22.0 in a sandboxed HOME, except 6 (OpenCode V2 source and docs) and 10 (CodeBuddy CLI). The credential leak in the local agent's .codebuddy/models.json, found in the same analysis, is handled inside #915.

# Bug Impact Relation to #915
1 A pull in a linked worktree deletes the main checkout's state when the repo uses --separate-git-dir or is a submodule data loss #915 builds on it (live checkouts)
2 Without a delivery record for the checkout, pull deletes or overwrites rules the member edited, while edited skills are kept data loss #915 builds on it (delivery records)
3 Codex skill delivery overwrites a member's own skill of the same name in .agents/skills/ data loss independent
4 pull writes .teamai/managed-hooks.json into the project tree, where git add -A commits it machine state committable independent
5 In a repo without .git/info/, pull says ".git/info is not writable" and withholds project MCP servers MCP withheld #915 reuses the same seam
6 OpenCode V2 gets no team rules or team instructions: V2 ignores the instructions entries teamai relies on rules and instructions missing #915 OpenCode slice depends on it
7 In project scope, the co-author setting is written into the team's tracked .claude/settings.json tracked file modified independent
8 Source skills go to the project's .hermes/skills and .openclaw/skills, which those tools never read, and to tools the member did not enable dead copies, unselected tools independent
9 teamai init . --agent claude still sets up every tool found in HOME --agent ignored independent
10 CodeBuddy reads only the first user MCP file that exists, so teamai's servers and the member's own servers hide each other MCP servers hidden independent
11 WorkBuddy's project MCP servers go to .workbuddy/mcp.json, which the WorkBuddy engine does not read (needs a check in the app) withdrawn: not a bug none
12 Without a delivery record, pull overwrites a member's own skill, rule, agent, doc or MCP server, and duplicates its own hook entries data loss #915 builds on it (foreign files)
1. A pull in a linked worktree deletes the main checkout's state when the repo uses --separate-git-dir or is a submodule

Problem

Each checkout of a repo has a delivery record in state.lastPullByWorkspace and a workspaces/<id>/ directory in the data home, which holds the managed-MCP record, search index and resource cache. A full pull drops the records and directories of checkouts that git worktree list no longer reports (#808, #812).

In a repo created with git init --separate-git-dir, and in a submodule, git worktree list --porcelain names the git directory as the main entry, not the main checkout. A pull in any linked worktree then treats the main checkout as removed. It deletes the main checkout's workspaces/<id>/ and its delivery record.

git worktree add is enough to trigger it, because teamai's post-checkout hook pulls in the new worktree.

With the record gone, the main checkout's next pull has nothing to compare against. It overwrites a rule or skill the member edited there, with no warning.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo.

mkdir sep sepgit && cd sep
git init -q -b main --separate-git-dir=../sepgit/proj.git
echo '# app' > README.md && git add -A && git commit -qm app
teamai init <team-url> --provider git --agent claude --role frontend --scope project
git worktree add -q ../sep-wt -b wt     # teamai's post-checkout hook pulls in sep-wt

The workspace IDs are sep=5caaebeed5d7 and sep-wt=821f29cdc362.

== after init in sep
  workspaces/: 5caaebeed5d7
  lastPullByWorkspace: ['5caaebeed5d7-361170810-1791360180782']
== after 'git worktree add ../sep-wt' (teamai's post-checkout pull ran there)
  workspaces/: 821f29cdc362
  lastPullByWorkspace: ['821f29cdc362-361171280-1791360182355']
== git worktree list --porcelain | grep ^worktree
worktree .../sepgit/proj.git
worktree .../sep-wt

Then, in the main checkout:

$ echo "member edit" >> .claude/rules/team-rule.md
$ teamai pull
✔ [project] Synced 1 skills (all updated)
✔ [project] Synced 2 rule(s)
$ cat .claude/rules/team-rule.md
# Team rule

The edit is gone, and no "Kept" line was printed. The same steps in a normal repo keep both workspace directories and both records, and keep the edit.

Expected: a pull in a linked worktree leaves the main checkout's directory and record alone. The main checkout's next pull then keeps the edited rule, as it does in a normal repo.

Cause

listWorktrees (src/utils/git.ts) returns the worktree lines of git worktree list --porcelain. In these layouts, the first entry is the git directory (.../sepgit/proj.git, or .../super/.git/modules/<path> for a submodule), so the main checkout is missing from the list. Two callers in src/pull.ts use that list as proof of liveness:

  • liveCheckoutRecords keeps only the records whose checkoutKey matches a listed root.
  • pruneWorkspaceDirs (src/utils/partition.ts) deletes every workspaces/<id>/ whose managedMcpWorkspaceId is not in the list.

Related: listWorktrees splits the porcelain output on \n. A checkout path that contains a newline is cut short, so that worktree also counts as removed. git worktree list --porcelain -z returns the full path in one NUL-terminated field.

Impact

Proposed fix

Define liveness by checking each recorded checkout, not by membership in the porcelain list. A checkout is live when its root exists and git -C <root> rev-parse --git-common-dir (realpath) equals this repo's common dir. Always count the current checkout as live.

When reading porcelain at all, use git worktree list --porcelain -z and skip entries marked bare or prunable, and an entry equal to the common dir. Apply the same rule in liveCheckoutRecords and pruneWorkspaceDirs.

Add regression cases for --separate-git-dir, a submodule, and a worktree path with a newline.

Not checked

  • The submodule case was not re-run here. Its porcelain shape (main entry .../.git/modules/<path>) is from the [feat] keep the resources pull delivers in project scope out of git #915 git-topology analysis.
  • The newline case was checked at the git level only: porcelain output and the parser's split. It was not run through teamai pull.
  • Windows.
  • -z needs git 2.36 or later. The minimum git version teamai supports was not checked.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Claude.

2. Without a delivery record for the checkout, pull deletes or overwrites rules the member edited, while edited skills are kept

Problem

Pull protects a copy the member edited only when the checkout has a delivery record (#822, #865). That record lives in state.lastPullByWorkspace under a key that includes the inode of the checkout's .git. When the record is missing, rule copies have no protection:

  • A rule the member edited and that is still delivered is overwritten.
  • A rule the member edited and that is no longer delivered (for example after teamai roles set) is deleted.

Neither case prints anything without --verbose. Skills behave differently. An edited skill that is no longer delivered is kept by a content check against the team repo, whether or not a record exists.

A record goes missing in normal use:

  • The checkout is restored from a backup, or copied (cp -a, rsync, Time Machine, a move across volumes). .git gets a new inode, so the key no longer matches.
  • A pull in a linked worktree prunes the main checkout's record in a --separate-git-dir repo or a submodule (bug 1 above).
  • listWorktrees misses a live checkout, for example one whose path contains a newline, so liveCheckoutRecords drops its record.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo. The team has roles frontend and backend, rules team-rule, frontend/fe-rule and backend/be-rule, and skills frontend/fe-skill and backend/be-skill.

cd biz
teamai init <team-url> --provider git --agent claude --role frontend --scope project
# give .git a new inode, as a restore or copy does
mv .git .git.old && cp -a .git.old .git && rm -rf .git.old
echo "member edit" >> .claude/skills/fe-skill/SKILL.md
echo "member edit" >> .claude/rules/frontend/fe-rule.md
echo "member edit" >> .claude/rules/team-rule.md
teamai roles set backend
teamai pull
✔ [project] Synced 1 skills (1 new, 0 updated)
✔ [project] Synced 2 rule(s)
⚠ [project] Kept skill "fe-skill" (claude): it has local changes or unpushed files not in the team repo (or could not be verified). ...
$ find .claude -type f | sort
.claude/rules/backend/be-rule.md
.claude/rules/team-rule.md
.claude/skills/be-skill/SKILL.md
.claude/skills/fe-skill/SKILL.md
.claude/skills/teamai/SKILL.md
$ cat .claude/rules/team-rule.md
# Team rule

With --verbose, the same run on a second fixture prints [debug] Removed stale rule frontend/fe-rule.md from claude. It also shows the record key changing from 6d08ff919a31-361166912 to 6d08ff919a31-361167393.

Expected:

  • frontend/fe-rule.md is kept with the "Kept ... you changed this copy" warning, as it is when a record exists.
  • team-rule.md keeps the member's line, with the "Kept ... you changed it" notice.

Cause

openCheckoutLedger (src/pull.ts) reads the record for checkoutKey(projectRoot). When there is none, ledger.previous is undefined, and src/resources/delivered-copies.ts treats every copy as unchanged:

  • judgeCopy returns write, so a still-delivered rule is overwritten.
  • removedCopyChanged returns false, so the stale sweep in RulesHandler.pullAllRules (src/resources/rules.ts) removes a rule that is no longer delivered. Claude is not in RULE_FORMATS, so it takes the branch that relies on removedCopyChanged.

The skill path does not depend on the record. Its removal checks the copy's content against the team repo and keeps it on any difference.

The #822 design chose "without a record, pull writes as it always has" for a first install. It did not account for a record that is lost after delivery.

Impact

The member's work in rule files is lost without a message: deleted in one case, overwritten in the other. Once #915 hides delivered files from git status, git also has no trace of them to recover.

Proposed fix

When the checkout has no record for a copy, take it as teamai's only on proof: its bytes equal what teamai renders for that resource at some revision in the team repo's history (the same proof bug 12 needs). An unedited copy from an older team version is then updated as today; an edited one is kept and named, as an edited copy with a record is named. This also covers rules a team once committed to the repo.

A first install still writes wherever no file exists. The record is never carried over to a new key: #807 puts the inode in the key so that a re-created worktree starts clean.

Not checked

  • Agents. They use the same ledger, so they probably behave like rules.
  • Tools other than Claude. Cursor takes the same branch as Claude, so it should lose rules the same way. Tools that share their rules directory with the member's own rules (sharesRulesDirWithMember: Copilot, CodeBuddy, Kiro and others) remove a copy only when recordedUnchanged proves it is unchanged. They should keep the copy when there is no record. Neither was run.
  • Windows, where checkoutKey uses a different inode and birth-time behavior.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Claude.

3. Codex skill delivery overwrites a member's own skill of the same name in .agents/skills/

Problem

For Codex, teamai delivers a skill into .agents/skills/<name>/ instead of .codex/skills/<name>/ whenever .agents/skills/<name>/ already exists (#435). Other tools share .agents/skills/, so it may hold a skill the member wrote, or one another tool installed. teamai treats it as its own and replaces the member's files with the team version. At init this happens silently. On a later pull it happens right after a warning that says both copies are kept.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo with the role skill frontend/fe-skill.

Case 1: the member's skill exists before they join the team.

cd biz
mkdir -p .agents/skills/fe-skill
printf -- '---\nname: fe-skill\ndescription: my own skill\n---\nMY OWN CONTENT\n' > .agents/skills/fe-skill/SKILL.md
teamai init <team-url> --provider git --agent codex --role frontend --scope project
✔ [project] Synced 1 skills (1 new, 0 updated)
    new: fe-skill
$ tail -1 .agents/skills/fe-skill/SKILL.md
# fe-skill
$ find .codex .agents -type f
.codex/skills/teamai/SKILL.md
.agents/skills/fe-skill/SKILL.md

The member's file was replaced, with no warning, and the skill was reported as "new".

Case 2: the member creates the skill after teamai delivered it to .codex/skills/.

teamai init <team-url> --provider git --agent codex --role frontend --scope project
mkdir -p .agents/skills/fe-skill
printf -- '---\nname: fe-skill\ndescription: my own skill\n---\nMY OWN CONTENT\n' > .agents/skills/fe-skill/SKILL.md
teamai pull --force
⚠ Codex skill conflict for fe-skill: keeping different copies in .agents/skills and .codex/skills
✔ [project] Synced 1 skills (all updated)
$ tail -1 .agents/skills/fe-skill/SKILL.md
# fe-skill

The warning says both copies are kept, but MY OWN CONTENT is gone.

Expected: teamai never wrote .agents/skills/fe-skill/, so the copy is the member's, and teamai leaves it alone. It delivers to .codex/skills/fe-skill/ and names the conflict.

Cause

resolveSkillDestination (src/resources/skills.ts) returns .agents/skills/<name> (SHARED_AGENT_SKILLS_PATH) for Codex whenever that path exists. When the two copies differ, it logs the conflict warning and still returns the shared path. SkillsHandler.pullItem then writes the team skill there.

The edit protection from #822 does not apply. judgeCopy (src/resources/delivered-copies.ts) finds no record for files teamai never wrote, and so treats the copy as safe to write.

Impact

Any directory in .agents/skills/ whose name matches a team skill is overwritten at init or on the next full pull. This includes a skill the member wrote, one installed by another tool, or one installed by hand. The loss is silent at init. On a later pull the only message is a warning that says the opposite.

Proposed fix

In resolveSkillDestination, return .agents/skills/<name> only when teamai owns that copy:

  • the checkout's delivery record lists files under it, or
  • its content equals the team skill at some revision in the team repo's history (the bug 12 proof), which includes the case the existing duplicate cleanup already handles.

teamai remove and teamai uninstall then delete only teamai's copy (.codex/skills/<name>) and leave a diverted member skill in .agents/skills/<name> alone.

Otherwise deliver to .codex/skills/<name>. Warn that Codex sees two skills with the same name and say which one is the member's. Change the warning text to match what happens.

Not checked

  • User scope (~/.agents/skills). The same function serves it, so it probably behaves the same, but it was not run.
  • Which skill Codex loads when both directories hold a skill with the same name.
  • Windows.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Codex (directory only; the Codex binary was not run).

4. pull writes .teamai/managed-hooks.json into the project tree, where git add -A commits it

Problem

Since the data home moved to ~/.teamai/projects/<slug>/ (#374), a project's working tree should hold no teamai machine state. One file still lands in it: .teamai/managed-hooks.json, the index of team hooks teamai injected. Nothing ignores it.

  • Git project scope with Copilot enabled: a pull writes .teamai/managed-hooks.json (the Copilot entry). It is the only file under .teamai/, and it shows as ??.
  • Self mode (init .): every pull with team hooks writes it. .teamai/.gitignore does not list it, so git add -A commits one member's hook index to the team's main branch.

teamai packages install has the same cause. It writes .teamai/teamai.lock into the project, then creates .teamai/.gitignore to hide it, and that file shows as ?? in a non-self project.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare repos as remotes.

Git project scope. The team repo has hooks/hooks.yaml with one Stop hook and sharing.hooks.autoApply: true. ~/.copilot exists.

cd biz
teamai init <team-url> --provider git --agent claude,copilot --role frontend --scope project
git status --porcelain -uall | grep -v 'skills/\|rules/'
?? .claude/settings.local.json
?? .github/hooks/teamai.json
?? .teamai/managed-hooks.json

.teamai/managed-hooks.json holds {"copilot": [...]}. The data home next to it already has managed-main-checkout-hooks.json, so the Claude and Codex index has moved and the Copilot index has not.

Self mode: teamai init . --provider git --agent claude, then commit .teamai/hooks/hooks.yaml with one hook and run teamai pull.

$ git status --porcelain -uall | grep teamai/
?? .teamai/managed-hooks.json
$ git check-ignore -v .teamai/managed-hooks.json || echo "not ignored"
not ignored

Packages: the team teamai.yaml declares packages.npm: [{name: left-pad, version: "1.3.0"}]. The project has a package.json, and a stub npm that exits 0 is on PATH.

$ teamai packages install
✔ TeamAI packages installed; wrote teamai.lock
$ git status --porcelain -uall --ignored | grep teamai/
?? .teamai/.gitignore
?? .teamai/managed-hooks.json
!! .teamai/teamai.lock

Expected: none of these files in the working tree. Each belongs in the data home, as managed-mcp.json and managed-main-checkout-hooks.json already do.

Cause

getManagedHooksPath (src/types.ts) builds the path from getTeamaiHome(scope, projectRoot), which is <projectRoot>/.teamai in project scope, not from getDataHome. Its callers include reconcileTeamHooksForConfig (src/hooks.ts, the Copilot manifest) and resolveHookScope (src/types.ts, self mode), plus hooks-cmd.ts and uninstall.ts.

pkgInstall and the package status check (src/pkg/commands.ts) also use getTeamaiHome for teamai.lock. ensurePackageLockIgnored (src/pkg/manifest.ts) then writes .teamai/.gitignore.

buildSelfModeGitignore (src/init.ts) lists teamai.lock and managed-mcp.json, but not managed-hooks.json.

Impact

  • In self mode, git add -A or a commit-all from an IDE publishes one member's hook index to the team. It contains the rendered hook commands for that member's role and projects.
  • In a git project, git status is never clean once Copilot or packages are in use.
  • If a teammate commits the file, a pull rewrites a tracked file. [feat] keep the resources pull delivers in project scope out of git #915 can't fix this by excluding the file, because it is machine state, not a delivered resource.

Proposed fix

  • Resolve getManagedHooksPath to the per-checkout workspace directory of the data home (workspaces/<id>/, where managed-mcp.json already lives), because the Copilot hook file and self-mode hooks are per checkout. Legacy readers (the pre-fix(hooks): unify hook-injection scope so project-scope init installs the SessionStart hook #370 Codex import, the legacy hook scope, uninstall) keep reading <root>/.teamai/managed-hooks.json through a separate helper. On the first pull after upgrade, move only the copilot key (project scope) or every key (self mode) out of the legacy file; delete the legacy file only when it is then empty and untracked, and have doctor name a tracked one.
  • Write teamai.lock to the data home and stop creating .teamai/.gitignore for it in a non-self project. Move an existing .teamai/teamai.lock, and remove a .teamai/.gitignore that holds only teamai's line.
  • Add managed-hooks.json to buildSelfModeGitignore, and to migrateSelfModeGitignore for existing installs.

Not checked

  • Windows.
  • A real npm install: a stub npm was used, so the lock content reflects the stub.
  • Whether any released version committed this file in a self-mode repo. A fix that relocates the file should also git rm --cached it, or doctor should name it, when it is tracked.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git.

5. In a repo without .git/info/, pull says ".git/info is not writable" and withholds project MCP servers

Problem

#886 keeps a project MCP config with resolved ${VAR} values out of git by adding it to .git/info/exclude before writing it. Some repos have no .git/info/ directory: those created with git init --template=, with an empty init.templateDir, or by some tools. In such a repo, teamai reports that .git/info "is not writable" and withholds every team MCP server whose config holds a resolved value. The directory is not read-only. It does not exist, and teamai could create it.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo:

  • mcp/mcp.yaml has one server, secret-api, with the header Authorization: Bearer ${LAB_TOKEN}.
  • env/env.yaml sets LAB_TOKEN.
  • sharing.mcp.autoApply: true.
mkdir biz && cd biz && echo '# app' > README.md
git init -q -b main --template= && git add -A && git commit -qm app
ls .git     # no info/
teamai init <team-url> --provider git --agent claude --role frontend --scope project
⚠ Did not write claude's MCP servers to .../biz/.mcp.json: it would hold resolved values, and teamai could not keep it out of git first: .../biz/.git/info is not writable. The file is left as it was. Make it writable, or add `/.mcp.json` to it yourself, then run `teamai pull` again.
⚠   ✖ MCP servers delivered to claude
    → In .../biz/.mcp.json, withheld: secret-api, as git would commit the file: .../biz/.git/info is not writable. ...

Afterwards, .mcp.json and .git/info are both absent. After mkdir .git/info && teamai pull --force, the server is written and .git/info/exclude holds the teamai block.

Expected: teamai creates .git/info/, adds /.mcp.json to exclude, and writes the server, with no warning.

Cause

ensureExcludedFromGit (src/mcp-git-exclude.ts) checks fse.access(path.dirname(excludeFile), W_OK) before it writes. access on a missing directory fails with ENOENT, which the function reports as "not writable". The write that follows, writeFileAtomic, would have created the directory with ensureDir.

The fix hint, "add /.mcp.json to it yourself", also points at the directory rather than the exclude file.

Impact

  • In these repos, members never get MCP servers whose config uses team env or secrets, and every pull and doctor run shows the warning.
  • The message sends the member to fix permissions that are not wrong.
  • [feat] keep the resources pull delivers in project scope out of git #915 plans to reuse this seam for its delivered block, so every pull in such a repo would fail to exclude delivered resources with the same wrong reason.

Proposed fix

In ensureExcludedFromGit, check writability on the nearest existing ancestor of the exclude file, and let the write create info/. Report "not writable" only for EACCES, EPERM or EROFS. Name the exclude file, not its directory, in the hint.

Add a test that removes .git/info and expects the server to be written and excluded.

Not checked

  • Linked worktrees and submodules without info/. The exclude path comes from git rev-parse --git-path info/exclude, so the fix should be the same.
  • Which clone tools or hosting setups produce repos without info/ in practice.
  • Windows.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Claude.

6. OpenCode V2 gets no team rules or team instructions: V2 ignores the instructions entries teamai relies on

Problem

teamai delivers team rules and its instruction file to OpenCode by listing them in an OpenCode config's instructions array:

  • Project scope: .opencode/opencode.json gets the .opencode/rules/**/*.md glob and .opencode/teamai-context.md.
  • User scope: ~/.config/opencode/opencode.json gets the user rules globs and the absolute path of the instruction file.

OpenCode V2 parses instructions and then ignores it. Members on OpenCode V2 get none of the team rules, and none of the culture, claudemd/ or recall blocks. teamai doctor still reports them as active.

#982 added V2 support to teamai's OpenCode plugin, but only for hooks.

Evidence

Source-level, OpenCode v2.0.24 (tag v2.0.24, commit e7a34f0, github.com/anomalyco/opencode):

  • packages/schema/src/config.ts declares instructions as an optional array of strings.
  • packages/core/src/config/plugin/instruction.ts is the only instruction source. It loads the global AGENTS.md and the AGENTS.md in each ancestor directory, and nothing else. No code in packages/ reads instructions from a config document. grep -rn -E '(info|config|document|doc|entry)\??\.instructions' packages returns nothing.
  • The V2 docs at that tag (instructions.mdx, https://opencode.ai/v2/docs/instructions/) say: "The V2 config schema accepts an instructions array, but V2 does not currently resolve its files, glob patterns, or URLs. This configuration does not add instructions to the model yet." They also say V2 "recognizes AGENTS.md only. It does not use CLAUDE.md as a fallback."

teamai side, built CLI 0.22.0 at f287dcb, sandboxed HOME with ~/.config/opencode:

teamai init <team-url> --provider git --agent opencode --role frontend --scope project
cat .opencode/opencode.json
teamai doctor | grep -i opencode
{
  "instructions": [
    ".opencode/rules/**/*.md"
  ]
}
  ✔ opencode is installed
  ✔ Skills delivered to opencode
  ✔ Team rules are active in opencode
  ✔ Rules delivered to opencode
  ✔ Team instructions are current for opencode

The installed plugin, ~/.config/opencode/plugin/teamai-hooks.ts, is built by buildDualPluginDefinition (src/opencode-hooks.ts). Its V2 setup(ctx) registers prompt and tool.execute.after hooks and an event subscription. It adds no context to the system prompt.

Cause

On V2, the only delivery channel teamai uses for rules and instructions is the instructions entry: opencodeProjectRuleGlobs, the user rules globs and opencodeContextReference (src/resources/opencode-config.ts). In user scope, opencodeClaudeFallback relies on OpenCode loading ~/.claude/CLAUDE.md, which V2 does not do either.

Impact

  • OpenCode V2 members silently work without the team's rules and instructions, in both scopes.
  • doctor reports success, so nothing points at the gap.
  • For [feat] keep the resources pull delivers in project scope out of git #915: the .opencode/opencode.json entries are the one OpenCode row that stays visible in a file the team may track. Delivering through the plugin would remove that row.

Proposed fix

Deliver through the plugin teamai already installs in HOME. In its V2 setup(ctx), register ctx.session.hook('context', ...) and the same text on compaction. The plugin reads the files itself: from ctx.location.directory it walks up to the nearest directory holding .opencode/teamai-context.md or .opencode/rules/, and pushes teamai-context.md followed by every .opencode/rules/**/*.md (the rules teamai delivered for the member's selection), sorted by path. In every directory it also pushes the user-scope rules and instruction file, as V1 does through the user config. When the user target is the CLAUDE.md fallback, teamai also writes ~/.config/opencode/teamai-context.md for V2.

Keep the instructions entries for V1, which does resolve them. Have doctor check the installed OpenCode major version and report V2 delivery from the plugin, not from instructions.

Not checked

  • An end-to-end V2 session. Confirming that the context hook reaches the model needs a model request, and none was made. Both the failure and the proposed hook rest on V2 source and docs.
  • Whether a later V2 release resolves instructions. The docs call it "not currently" supported, and migrate-v1.mdx lists instructions as needing no migration, which contradicts them.
  • OpenCode V1 is unaffected according to its source (packages/opencode/src/session/instruction.ts at v1.18.35). It was not run.

Environment

macOS 27.0, Node.js 24.21.0, teamai 0.22.0 (f287dcb), OpenCode v2.0.24 (source and docs; the installed binary reports opencode v2.0.24).

7. In project scope, the co-author setting is written into the team's tracked .claude/settings.json

Problem

When the team sets sharing.coAuthor.enabled: false, or a member sets coAuthorEnabled, teamai writes attribution into the project's .claude/settings.json. It also writes to .codebuddy/settings.json, which the #915 delivery inventory observed, and to the settings of other Claude-family tools. Many teams track .claude/settings.json, so the first pull leaves it modified in every member's checkout. teamai also rewrites the whole file, which reformats it.

Since #955 (PR #958), team hooks in project scope go to .claude/settings.local.json to keep the shared file clean. The co-author reconcile still writes to the shared file.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo whose teamai.yaml has sharing.coAuthor.enabled: false. The business repo tracks .claude/settings.json:

{
  "permissions": {
    "allow": ["Bash(npm test)"]
  }
}
teamai init <team-url> --provider git --agent claude --role frontend --scope project
git status --porcelain | grep -v '^??'
git diff
ℹ Co-author trailer disabled for claude. Restart your AI tool session to apply.
 M .claude/settings.json
 {
   "permissions": {
-    "allow": ["Bash(npm test)"]
+    "allow": [
+      "Bash(npm test)"
+    ]
+  },
+  "attribution": {
+    "commit": "",
+    "pr": ""
   }
 }

Expected: .claude/settings.json is unchanged. The setting goes to a file that is local to the member.

Cause

resolveTargets (src/coauthor-reconcile.ts) targets path.join(baseDir, paths.settings) for the Claude family, and baseDir is the project root in project scope. applyClaude reads, patches and rewrites that file with writeJson. Codex and Cursor are already skipped in project scope ("co-author is user-scope only"). The Claude family is not.

Impact

  • Every member's checkout shows a modified tracked file after the first pull.
  • If one member commits it, their co-author choice becomes the project's. coAuthorEnabled is a per-member override, so a member with a different setting rewrites the tracked file on their next pull.
  • The rewrite also reformats the file, so the diff is larger than the change.
  • [feat] keep the resources pull delivers in project scope out of git #915 promises that a pull leaves git status clean. This file is shared content, so excluding it is not an option and the fix has to be here.

Proposed fix

In project scope, write the Claude family's attribution to settings.local.json, the same personal layer #955 uses for team hooks. Do the same for CodeBuddy if it has a local settings layer. Otherwise skip it in project scope, as Codex and Cursor are skipped.

On the next pull, remove an attribution that teamai wrote to the shared file before the fix, but only when it is teamai's {"commit": "", "pr": ""} and coAuthorManaged records the file.

Not checked

  • That Claude Code reads attribution from settings.local.json. Its settings documentation says local project settings override shared ones, but this was not run.
  • Whether CodeBuddy has a local settings layer.
  • claude-internal, tclaude, Qoder and WorkBuddy, which the [feat] keep the resources pull delivers in project scope out of git #915 inventory lists with the same writer. Not run.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Claude.

8. Source skills go to the project's .hermes/skills and .openclaw/skills, which those tools never read, and to tools the member did not enable

Problem

Skills from a team's sources: take a different delivery path from the team's own skills, and they land in the wrong places:

  • Hermes and OpenClaw get the team's skills in their homes (~/.hermes/skills, <openclaw workspace>/skills). Source skills go instead to <project>/.hermes/skills/ and <project>/.openclaw/skills/, which neither tool reads. Hermes and OpenClaw members never receive source skills, and the project gets two dead copies.
  • Any tool directory that exists in the project receives source skills, even when that tool is not in enabledAgents.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME with ~/.hermes and ~/.openclaw/workspace, local bare repos.

The team teamai.yaml has sources: [{name: other, repo: <other-url>}]. The source repo publishes other-skill, and the team has the role skill frontend/fe-skill.

cd biz
teamai init <team-url> --provider git --agent claude,hermes,openclaw --role frontend --scope project
git status --porcelain -uall | grep skills
find ~/.hermes ~/.openclaw -name SKILL.md

Output, with the sandbox HOME shortened to ~:

?? .claude/skills/fe-skill/SKILL.md
?? .claude/skills/other-skill/SKILL.md
?? .claude/skills/teamai/SKILL.md
?? .hermes/skills/other-skill/SKILL.md
?? .openclaw/skills/other-skill/SKILL.md

~/.hermes/skills/teamai/SKILL.md
~/.hermes/skills/fe-skill/SKILL.md
~/.openclaw/workspace/skills/teamai/SKILL.md
~/.openclaw/workspace/skills/fe-skill/SKILL.md

Then, with enabledAgents: [claude, hermes, openclaw]:

$ mkdir .cursor && teamai pull --force
$ find .cursor -type f
.cursor/skills/other-skill/SKILL.md

Expected:

  • other-skill lands where fe-skill lands for each tool: ~/.hermes/skills/ and <openclaw workspace>/skills/.
  • Nothing is written to <project>/.hermes/ or <project>/.openclaw/.
  • Nothing is written to .cursor/, which is not enabled.

Cause

pullSingleSource (src/source.ts) chooses its targets with its own loop over scopedToolPaths:

  • It checks ResourceHandler.isToolInstalled(toolPath.skills, baseDir), where baseDir is the project root, and then calls resolveSkillDestination.
  • It does not use skillsDirForTool (src/resources/skills.ts), which sends Hermes to its home and OpenClaw to its workspace for the team's own skills.
  • It has no isAgentExcluded check, so enabledAgents and disabledAgents do not apply.

Impact

  • Hermes and OpenClaw members never get the skills their team subscribes to.
  • Projects collect untracked .hermes/ and .openclaw/ copies that nothing loads. A git add -A commits them.
  • A member who limits teamai to some tools still gets source skills in every other tool directory that exists in the project.
  • [feat] keep the resources pull delivers in project scope out of git #915 would add these paths to the exclude block as delivered paths. That hides them, but the fix belongs here.

Proposed fix

Resolve source skill targets through the same seam as team skills: skillsDirForTool for the directory, then resolveSkillDestination, after an isAgentExcluded check.

The source manifest records project-relative paths and rejects any path outside the project root, and the Hermes home and OpenClaw workspace are shared by every project subscribed to the same source. Record those destinations as absolute paths in the source manifest, owned per (source, project), and remove a copy there only when no other project's manifest for the same source still lists it, as team skills in those homes already work.

On the next pull, reclaim the copies an earlier pull left in <project>/.hermes/skills/ and <project>/.openclaw/skills/, using the source's install records. Reclaim a copy only when it equals the source skill at some revision of the source repo's history (the bug 12 proof); keep and name a changed one.

Add a test that delivers one source skill and one team skill to every built-in tool and asserts that both land in the same directory.

Not checked

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git. Hermes and OpenClaw were not run; only their directories exist.

9. teamai init . --agent claude still sets up every tool found in HOME

Problem

In single-repo mode, --agent is meant to choose which tools teamai sets up in the repo. On main it has no effect. teamai init . --agent claude still:

  • injects hooks into the project for every tool that has a directory in HOME (Codex, Cursor, CodeBuddy, Copilot and so on),
  • saves all of them in enabledAgents.

Every later pull then delivers skills and hooks to those tools as well. The extra hook files are never committed, so they stay as untracked files in the team's repo.

Reproduction

Built CLI 0.22.0 at f287dcb. Sandboxed HOME with ~/.claude, ~/.codex, ~/.cursor, ~/.codebuddy and ~/.copilot. Repo self, whose origin is a local bare repo. init . requires an https origin, so the lab exposed it as one.

cd self
teamai init . --provider git --agent claude --verbose
git status --porcelain -uall
grep -A6 enabledAgents ~/.teamai/projects/*/config.yaml
✔ Created .teamai/teamai.yaml (mode: self)
✔ Updated teamai hooks in .../self/.claude/settings.json
✔ Updated teamai hooks in .../self/.codex/hooks.json
✔ Updated teamai hooks in .../self/.cursor/hooks.json
✔ Updated teamai hooks in .../self/.codebuddy/settings.json
✔ Updated teamai hooks in .../self/.github/hooks/teamai.json
  [debug] git hook: installed teamai hooks in .../self
  [debug] [bootstrap] Codex trust: trusted
✔ Local config saved to .../config.yaml
  [debug] Seeded tool dirs for: claude, codex, cursor, copilot, codebuddy
  [debug] teamai hooks already up-to-date in .../self/.claude/settings.json
✔ Committed .teamai/ skeleton to the current branch

?? .codebuddy/settings.json
?? .codex/hooks.json
?? .cursor/hooks.json
?? .github/hooks/teamai.json

enabledAgents:
  - claude
  - codex
  - cursor
  - copilot
  - codebuddy

The [bootstrap] line, and the hook writes before "Local config saved", come from the clone-time self-heal, not from init. After a team hook is added and a pull runs, .codex/skills/, .cursor/skills/, .codebuddy/skills/ and .github/skills/ also receive the teamai skill.

Expected:

  • enabledAgents: [claude].
  • Hooks only in .claude/settings.json, which init commits.
  • No files for the other tools.

Cause

initSelfRepo (src/init.ts) writes .teamai/teamai.yaml with mode: self. Only later does it call loadLocalConfigForScope('project', businessRepoRoot) to read the existing config.

With mode: self on disk and no config yet, that load runs the clone-time self-heal: readConfigFrom → bootstrapSelfRepo (src/config.ts, src/bootstrap.ts). The self-heal sets enabledAgents to detectHomeInstalledAgents(), injects hooks for all of them and saves the config. initSelfRepo then takes the union of that "existing" config and --agent, so the filter never applies.

PR #852 diagnosed the same cause ("Bug 2") and fixed it by reading the config once, before teamai.yaml is written. It was closed without merging. The dry-run half was later handled by refusing init --dry-run (#900). The --agent half is still open.

Impact

  • --agent does nothing in single-repo mode whenever other tools are installed in HOME.
  • The repo gets untracked hook files for tools the team never chose. A later git add -A commits them.
  • Every member's pulls deliver to tools they did not select.
  • init also runs a full self-heal as a side effect, including registering git hooks and Codex trust, before its own steps.

Proposed fix

Read the pre-init project config once, before teamai.yaml is written, and use that snapshot for every later step: inheritUserScope, the enabledAgents union, the tool-roots carry-over and settleModeSwitch. This is #852's approach.

Alternatively, give loadLocalConfigForScope a no-self-heal option for init. Either way, add a test that runs init . --agent claude with other tools in HOME and asserts enabledAgents: [claude] and no other tool's files.

Not checked

  • The interactive picker path (no --agent, with a TTY). It probably merges in the same way.
  • Re-running init . on a repo that already has a config. There the load returns the real config and the self-heal does not run.
  • Windows.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, single-repo mode.

10. CodeBuddy reads only the first user MCP file that exists, so teamai's servers and the member's own servers hide each other

Problem

In user scope, CodeBuddy reads exactly one MCP file: the first that exists of ~/.codebuddy/.mcp.json, ~/.codebuddy/mcp.json and ~/.codebuddy.json. It does not merge them. teamai always writes the second one, ~/.codebuddy/mcp.json. So:

  • A member who added a server with codebuddy mcp add -s user has ~/.codebuddy/.mcp.json. teamai's team servers are then never loaded, and nothing says so.
  • A member whose servers live in the legacy ~/.codebuddy.json loses them as soon as teamai creates ~/.codebuddy/mcp.json.

Reproduction

CodeBuddy Code 2.161.4, sandboxed HOME.

  • With teamai's ~/.codebuddy/mcp.json present, the member's usr-srv in ~/.codebuddy.json disappears from codebuddy mcp list.
  • With ~/.codebuddy/.mcp.json present, teamai's tm-user disappears.
  • In a fresh home, codebuddy mcp add -s user my-user ... creates ~/.codebuddy/.mcp.json; after that, teamai's tm-user is no longer listed.

Servers in the local scope (~/.codebuddy.json → projects[...]) are unaffected.

The CodeBuddy docs state the rule: "The system does not merge multiple configuration files within the same scope; it only uses the first existing file." (https://www.workbuddy.ai/docs/cli/mcp)

Cause

toolPaths.codebuddy.mcp (src/types.ts) is fixed to .codebuddy/mcp.json, the second candidate in CodeBuddy's lookup order.

Impact

Team MCP servers silently fail to load for members who use CodeBuddy's own mcp add, or teamai silently hides the member's existing servers. doctor reports the team servers as delivered in both cases.

Proposed fix

In user scope, write to the first CodeBuddy user MCP file that exists, and only create ~/.codebuddy/.mcp.json (CodeBuddy's recommended file) when none exists. Have doctor warn when the file teamai writes is not the one CodeBuddy reads. WorkBuddy is not affected: its app reads ~/.workbuddy/mcp.json, the file teamai writes (confirmed in the app).

Not checked

  • Whether an existing ~/.workbuddy/.mcp.json shadows ~/.workbuddy/mcp.json in the WorkBuddy app.
  • Migrating servers teamai already wrote to ~/.codebuddy/mcp.json when the member also has ~/.codebuddy/.mcp.json.

Environment

macOS 27.0, CodeBuddy Code 2.161.4, teamai 0.22.0 (f287dcb).

11. Withdrawn: WorkBuddy's project MCP servers go to .workbuddy/mcp.json, which the WorkBuddy engine does not read (needs a check in the app)

Withdrawn 2026-10-07: not a bug. The WorkBuddy app reads <project>/.workbuddy/mcp.json and ~/.workbuddy/mcp.json itself (Sidebar → Plugins (插件) → MCP Server (MCP 服务器), or Connectors → Custom Connector → Configure MCP (配置 MCP)), so teamai's target is correct; the engine run alone does not read it, which is what this report observed.

Problem

In project scope teamai writes WorkBuddy's team MCP servers to <project>/.workbuddy/mcp.json (toolPaths.workbuddy.mcpProject in src/types.ts; docs/usage-guide.md). The engine WorkBuddy runs (the CodeBuddy engine) reads project servers only from <project>/.mcp.json or <project>/mcp.json. If the desktop app does not read .workbuddy/mcp.json itself, WorkBuddy members get no team project servers from it.

Evidence

  • Engine, verified on the CodeBuddy Code 2.161.4 CLI run the way WorkBuddy runs it (CODEBUDDY_CONFIG_DIR=WORKBUDDY_CONFIG_DIR=$HOME/.workbuddy), with .mcp.json (proj-srv) and .workbuddy/mcp.json (in-wb-mcp) in the project: codebuddy mcp list shows proj-srv, the user servers in ~/.workbuddy/mcp.json and the local ones in ~/.workbuddy/.codebuddy.json, but not in-wb-mcp.
  • Docs: the CodeBuddy CLI MCP docs list only <project root>/.mcp.json and <project root>/mcp.json for project scope (https://www.workbuddy.ai/docs/cli/mcp). The official WorkBuddy MCP guide gives no file paths (https://www.workbuddy.ai/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/MCP-Guide). A third-party article names <project>/.workbuddy/mcp.json as the project level, citing that guide, without testing it (https://blog.sandbase.ai/workbuddy-mcp-setup-permissions-2026/).
  • The engine accepts servers passed in by the app (marked _workbuddyMcpOrigin: "custom"), so the app could read .workbuddy/mcp.json itself. That was not checked.

Check needed (one session with the WorkBuddy app)

  1. Put a server only in <project>/.workbuddy/mcp.json, open the project, and see whether it is listed.
  2. Put one in ~/.workbuddy/.codebuddy.json under projects["<opened folder realpath>"].mcpServers and see whether it is listed.

If check 1 fails, this bug is confirmed. If check 2 passes, the fix is to deliver WorkBuddy's project servers to that local scope, keyed by the realpath of each worktree root (the key CodeBuddy uses), which also keeps them out of the repo.

Impact (if confirmed)

WorkBuddy-only members get no team project MCP servers; members who also have Claude or CodeBuddy get them only through .mcp.json. doctor reports them as delivered.

Environment

macOS 27.0, CodeBuddy Code 2.161.4 (engine), teamai 0.22.0 (f287dcb). WorkBuddy desktop not run.

12. Without a delivery record, pull overwrites a member's own skill, rule, agent, doc or MCP server, and duplicates its own hook entries

Added 2026-10-07 after a review of the #915 plan.

Problem

When a project tool folder already holds a file or directory at the path where teamai delivers a team resource, or an MCP config already holds a server with the same name as a team server, and the checkout has no delivery record for it, pull and init replace it with the team version without a word. The output reports the resource as synced.

It hits a member who wrote a skill, rule or agent before joining the team, a resource installed by another tool or by hand, a member's own MCP server that happens to share a team server's name, and any copy whose record was lost (bug 1, bug 2). Bug 3 is the Codex .agents/skills case of the same failure.

Reproduction

Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo with a skill team-skill, a rule team-rule, an agent team-agent, a doc guide.md, a claudemd/ fragment, an MCP server plain-api and a Stop team hook. Before init, the business repo holds the member's own files at the same paths and names:

cd biz
# the member's own files, written by hand
.claude/skills/team-skill/SKILL.md      "MY SKILL"
.claude/rules/team-rule.md              "MY RULE"
.claude/agents/team-agent.md            "MY AGENT"
.teamai/docs/guide.md                   "MY GUIDE"
.claude/rules/teamai-context.md         "MY CONTEXT"
.mcp.json                               plain-api -> https://mine.example.com/mcp
.claude/settings.local.json             Stop hook "echo my-stop"
teamai init <team-url> --provider git --agent claude --scope project
teamai pull --force
Member's file After init and pull Message
skill team version none ("Synced 1 skills (all updated)")
rule team version none
agent team version none
doc team version none
MCP server plain-api team URL "MCP: 1 change(s) across 1 server(s)"
teamai-context.md kept "was not written by teamai, so teamai left it unchanged"
Stop hook kept; teamai adds its own marked entry none needed

A second run shows the other side of the same cause. With teamai's main-checkout hook manifest removed (as when the record is lost), teamai pull --force adds a second, identical [teamai:hook:team-stop] entry to .claude/settings.local.json next to the one it wrote before, so the team hook runs twice. The usage guide states that "unrecorded or ambiguous legacy team-hook copies are preserved", and a new copy is then written.

Expected: every resource behaves like teamai-context.md: the member's file or server is left alone and named, and the team version is not delivered there until the member resolves it.

Cause

Ownership without a record defaults to teamai:

  • Skills and agents: SkillsHandler.pullItem and the agents handler skip a destination only when keepsEditedCopy says so; classifyCopy (src/resources/delivered-copies.ts) keeps a copy only when a file has a recorded hash. A destination with no record is always written.
  • Rules: the same ledger (judgeCopy); bug 2 is the lost-record case of it.
  • Docs: DocsHandler.pullDocs writes the mirror without checking an existing file it never wrote.
  • MCP: the reconcile writes team servers by name; a same-name server not in teamai's MCP manifest is replaced.
  • Hooks: the hook reconcile owns only entries its manifest records; an unrecorded copy teamai wrote earlier is preserved, and a new one is added.

The safe cases already prove ownership in the file: teamai-context targets check teamai's markers (planFile), and hooks write marked entries next to the member's.

Impact

A member's skill, rule, agent, doc or MCP server is lost the first time the team publishes one with the same name, or on the first pull after the record is lost. Only the MCP case prints anything, and it reads as a normal change.

Proposed fix

One ownership rule for every resource without a record, the one bug 2 needs too:

  • A destination that does not exist is written, as today (first install).
  • An existing file or directory with no record is teamai's only on proof: its content equals that resource at some revision in the team repo's history (for a source skill, the source repo's), compared by git blob ids. An older team copy is then updated as today.
  • Otherwise it is foreign: not written, named in the pull output with the path and how to resolve it (rename or delete it, then pull), and the team version is not delivered there. doctor lists it.
  • Entries in shared config files (MCP servers, hook entries in tool settings and hook files): an entry with no record that exactly equals what teamai renders for a team server or hook, at the current revision or any revision in the team repo's history, and matches only one, is adopted into teamai's manifest and then updated or removed as teamai's. Any other unrecorded entry is the member's: kept and named. For MCP, a same-name member server means the team server is not written to that file.
  • An existing record keeps today's behavior.

Not checked

  • Tools other than Claude; user scope.
  • The cost of the history comparison on large team repos.
  • .github/copilot-instructions.md and the CLAUDE.md blocks of the internal Claude variants (they use marked blocks, like teamai-context).
  • Hook duplication was run for Claude's settings.local.json; Codex hooks.json and other hook files follow the same manifest logic and were not run.

Environment

macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Claude.

Activity

  1. self-assigned this
    on Oct 7, 2026
  2. 139 remaining items

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions