Description
In single-repo (kind: self) mode, the side-branch checkouts and the contribution queue live inside each worktree. Git lets a branch be checked out in one worktree only, so every worktree after the first can't publish learnings or reports, and a learning queued in a worktree is deleted with it.
Git and http mode are not affected: there these paths are in the shared partition. The real CLI confirmed that contributions from two worktrees both publish there.
Cause. For a self config, readConfigFrom re-anchors repo.localPath to <workspaceRoot>/.teamai (src/config.ts:507). That is intentional, because committed knowledge follows the branch. Several things that belong to the project are derived from localPath too:
<workspaceRoot>/.teamai/ one per worktree
├── learnings-wt/ checkout of teamai-learnings getWorktreeDir src/types.ts:2164
├── reports-wt/ checkout of teamai-reports getWorktreeDir
└── pending-learnings/ queue, gitignored pendingLearningsDir src/utils/pending-learnings.ts:20
~/.teamai/projects/<slug>/ one per project
└── search-index.json built from the per-worktree paths above
What breaks
Scenario: repo at /r, worktrees /r/.claude/worktrees/a and /elsewhere/b. Unless noted, the real CLI reproduced each item.
| Symptom |
Where |
The first checkout to create .teamai/learnings-wt owns teamai-learnings. contribute and pull from every other checkout fail with fatal: 'teamai-learnings' is already used by worktree at …, on every retry. A linked worktree that got there first locks out the main checkout the same way |
ensureWorktree, src/utils/branch-worktree.ts:158-250 |
A learning queued in b is deleted when b is removed. A plain git worktree remove does it without --force, because ignored files don't make a worktree dirty |
pendingLearningsDir |
Same lock-out for teamai-reports. From a non-owning checkout, pull reports nothing, stats omits the reported totals, digest fails with the raw git error, and votes-sync doesn't push. The data stays pending, so nothing is lost |
getWorktreeDir |
recall serves whatever checkout rebuilt the shared index last. After a contribute in b, recall in /r returned only b's learning, with a path into b, and /r's own published learning was missing. After b was removed, the path pointed nowhere |
src/recall.ts:297-300, src/pull.ts:984-999, src/contribute.ts:65-81 |
Suspected, not run: import --from-mr in a checkout without a valid learnings-wt writes into a plain directory, which the next ensureWorktree removes (fse.remove) |
src/import.ts:246-252, src/utils/branch-worktree.ts:193-198 |
Proposed fix
- Side-branch checkouts move to the partition. In self mode,
getWorktreeDir returns <getDataHome()>/<dirname>. git worktree add accepts a path outside the repo, and gitRoot stays the business repo, whose refs every worktree shares. The lock (dirname(worktreePath)) moves with it.
- The queue moves to the partition. In self mode,
pendingLearningsDir returns <getDataHome()>/pending-learnings.
- Per-checkout index. In self mode, docs, rules and skills really do differ per branch. Key the index per workspace,
workspaces/<managedMcpWorkspaceId(workspaceRoot)>/search-index.json, the same way managed MCP is keyed.
- One-time migration. Move each existing
<worktree>/.teamai/pending-learnings/* into the partition queue, then run git worktree prune, and remove the old .teamai/*-wt registrations.
// src/types.ts getWorktreeDir
if (isSelfMode(localConfig)) {
- return path.join(localConfig.repo.localPath, dirname);
+ return path.join(getDataHome(localConfig), dirname);
}
// src/utils/pending-learnings.ts pendingLearningsDir
if (isSelfMode(localConfig)) {
- return path.join(localConfig.repo.localPath, 'pending-learnings');
+ return path.join(getDataHome(localConfig), 'pending-learnings');
}
Not in scope
- A separate suspicion, not tied to worktrees:
autoPushViaMR commits in the knowledge clone, not in learnings-wt, so an MR-imported learning may never be pushed. It needs its own check.
- Migrate edge cases for legacy per-worktree
.teamai/ directories (renamed to .bak without draining their queue; a per-worktree migration lock with shared staging). These are suspected and become rare once the queue lives in the partition.
Docs and skills
docs/designs/data-directory-layout.md: the self-mode rows for learnings-wt, reports-wt and pending-learnings.
skill-data/ (share, core): grep for pending-learnings and .teamai/learnings-wt.
Reproduction
- Set up a self-mode project at
/r with a bare origin, then run teamai pull.
teamai contribute in /r succeeds.
git worktree add ../b -b b, cd ../b, teamai contribute. It saves locally with fatal: 'teamai-learnings' is already used by worktree at '/r/.teamai/learnings-wt'.
git worktree remove ../b. The learning queued in step 3 is gone.
Acceptance
- Contributions from
/r and from b both reach origin/teamai-learnings, whichever runs first.
- Removing
b after a contribute there loses nothing.
recall in /r returns both learnings, with paths that exist.
- Regression test: a real-git test with a main checkout plus one linked worktree in self mode, contributing from both.
src/__tests__/contribute-self-learnings.test.ts uses a single checkout today.
Environment
- OS: macOS (Darwin 25.5.0)
- teamai: 0.22.0 (
dist built from fd0e913)
- Provider: GitHub (self mode)
- AI tool(s): Claude Code
Description
In single-repo (
kind: self) mode, the side-branch checkouts and the contribution queue live inside each worktree. Git lets a branch be checked out in one worktree only, so every worktree after the first can't publish learnings or reports, and a learning queued in a worktree is deleted with it.Git and http mode are not affected: there these paths are in the shared partition. The real CLI confirmed that contributions from two worktrees both publish there.
Cause. For a self config,
readConfigFromre-anchorsrepo.localPathto<workspaceRoot>/.teamai(src/config.ts:507). That is intentional, because committed knowledge follows the branch. Several things that belong to the project are derived fromlocalPathtoo:What breaks
Scenario: repo at
/r, worktrees/r/.claude/worktrees/aand/elsewhere/b. Unless noted, the real CLI reproduced each item..teamai/learnings-wtownsteamai-learnings.contributeandpullfrom every other checkout fail withfatal: 'teamai-learnings' is already used by worktree at …, on every retry. A linked worktree that got there first locks out the main checkout the same wayensureWorktree,src/utils/branch-worktree.ts:158-250bis deleted whenbis removed. A plaingit worktree removedoes it without--force, because ignored files don't make a worktree dirtypendingLearningsDirteamai-reports. From a non-owning checkout,pullreports nothing,statsomits the reported totals,digestfails with the raw git error, andvotes-syncdoesn't push. The data stays pending, so nothing is lostgetWorktreeDirrecallserves whatever checkout rebuilt the shared index last. After a contribute inb, recall in/rreturned onlyb's learning, with a path intob, and/r's own published learning was missing. Afterbwas removed, the path pointed nowheresrc/recall.ts:297-300,src/pull.ts:984-999,src/contribute.ts:65-81import --from-mrin a checkout without a validlearnings-wtwrites into a plain directory, which the nextensureWorktreeremoves (fse.remove)src/import.ts:246-252,src/utils/branch-worktree.ts:193-198Proposed fix
getWorktreeDirreturns<getDataHome()>/<dirname>.git worktree addaccepts a path outside the repo, andgitRootstays the business repo, whose refs every worktree shares. The lock (dirname(worktreePath)) moves with it.pendingLearningsDirreturns<getDataHome()>/pending-learnings.workspaces/<managedMcpWorkspaceId(workspaceRoot)>/search-index.json, the same way managed MCP is keyed.<worktree>/.teamai/pending-learnings/*into the partition queue, then rungit worktree prune, and remove the old.teamai/*-wtregistrations.// src/types.ts getWorktreeDir if (isSelfMode(localConfig)) { - return path.join(localConfig.repo.localPath, dirname); + return path.join(getDataHome(localConfig), dirname); } // src/utils/pending-learnings.ts pendingLearningsDir if (isSelfMode(localConfig)) { - return path.join(localConfig.repo.localPath, 'pending-learnings'); + return path.join(getDataHome(localConfig), 'pending-learnings'); }Not in scope
autoPushViaMRcommits in the knowledge clone, not inlearnings-wt, so an MR-imported learning may never be pushed. It needs its own check..teamai/directories (renamed to.bakwithout draining their queue; a per-worktree migration lock with shared staging). These are suspected and become rare once the queue lives in the partition.Docs and skills
docs/designs/data-directory-layout.md: the self-mode rows forlearnings-wt,reports-wtandpending-learnings.skill-data/(share,core): grep forpending-learningsand.teamai/learnings-wt.Reproduction
/rwith a bare origin, then runteamai pull.teamai contributein/rsucceeds.git worktree add ../b -b b,cd ../b,teamai contribute. It saves locally withfatal: 'teamai-learnings' is already used by worktree at '/r/.teamai/learnings-wt'.git worktree remove ../b. The learning queued in step 3 is gone.Acceptance
/rand frombboth reachorigin/teamai-learnings, whichever runs first.bafter a contribute there loses nothing.recallin/rreturns both learnings, with paths that exist.src/__tests__/contribute-self-learnings.test.tsuses a single checkout today.Environment
distbuilt fromfd0e913)