Repository navigation
fix: delivery and local-state bugs (#993) - #995
Conversation
… the plugin OpenCode V2 parses `instructions` and ignores it, so a V2 session got none of the team rules or the culture, claudemd and recall blocks. The teamai plugin's V2 setup now registers the session context and compaction hooks and adds the user instruction file and rules, then the nearest project's .opencode/teamai-context.md and .opencode/rules/**/*.md, sorted by path. V1 delivery through `instructions` is unchanged. (Tencent#993)
…back for V2 OpenCode V1 reads ~/.claude/CLAUDE.md while ~/.config/opencode/AGENTS.md does not exist, so teamai wrote no OpenCode user file there. V2 reads neither that fallback nor `instructions`. Pull and HTTP prompts now write ~/.config/opencode/teamai-context.md for teamai's plugin in that case too, and still list it nowhere, so V1 gets no duplicate. (Tencent#993)
…#993) init . wrote .teamai/teamai.yaml (mode: self) before it read the existing project config. With the marker on disk and no config yet, that read ran the clone-time self-heal, which enabled every tool found in HOME, injected their hooks into the repo and saved them; init then merged --agent into that. Read the config once, before the marker is written, and use that snapshot for inheriting user scope, the enabledAgents union, the tool-roots carry-over and the mode-switch settlement.
… settings (Tencent#993) In project scope only claude writes attribution, to the member-local .claude/settings.local.json; the rest of the Claude settings family is user-scope only, like Codex and Cursor. The next pull removes an attribution an earlier release wrote to a shared project settings file when it is exactly teamai's value and the co-author record lists the file, deleting only that member's text.
doctor reported the team rules and instructions active on OpenCode V2 because `instructions` listed them, but V2 ignores that key. doctor now reads the major version from `opencode --version` (missing or unparsable counts as V1) and, on V2, checks that teamai's plugin in HOME is the one this build writes, since its context hook is what delivers them. V1 checks are unchanged. The usage guide describes V2 delivery. (Tencent#993)
… list (Tencent#993) A pull in a linked worktree of a --separate-git-dir repo or a submodule dropped the main checkout's delivery record and workspaces/<id>/, because git worktree list names the git directory there instead of the main checkout. Its next pull then overwrote edited rules without a word. Records and workspace directories now store their checkout root; a full pull keeps one while that root still has a .git, names the same git common dir, and (records) gives the same checkoutKey, so a re-created worktree still starts fresh (Tencent#807). Entries without a root (older CLI) are kept. listWorktrees reads --porcelain -z, skipping bare, prunable and common-dir entries, and returns [] when -z is unsupported instead of splitting lines.
…one (Tencent#993) A fresh clone whose committed .teamai/teamai.yaml says mode: self ran the self-heal in the preAction queue check and again in initSelfRepo's config read. Either one enabled every tool found in HOME before --agent could choose. init now loads with selfHeal: false in both places; every other command still self-heals.
…mber's (Tencent#993) An MCP server or a team hook entry teamai has no record of is teamai's only when it equals teamai's render of a team server or hook, today or at any revision in the team repo's history (one hook only). It is then adopted and handled as recorded. Any other one is the member's: kept and named by pull and doctor; a member's same-name MCP server means the team server is not written to that file. `pull --force` no longer overwrites a member's server (`teamai mcp inject --force` still does). This removes the second copy of each team hook a lost hook manifest caused in .claude/settings.local.json and .codex/hooks.json.
…o terminal (Tencent#993) With the clone self-heal skipped for init, a fresh clone has no config when init runs. Pin that it takes the non-interactive default (the tools found in HOME) instead of prompting or stopping at "already initialized".
…tings.local.json (Tencent#993)
…orktree list names every checkout (Tencent#993) contribute, recall, viz, the MCP writers and the local agent create a checkout's workspaces/<id>/ before its first full pull writes root, so a removed worktree that never ran a full pull kept its directory forever (self-mode-worktrees-808). Such a directory now goes when git worktree list -z names every checkout (no non-bare entry is the common dir) and none has its id, as git prints it or realpath'd. It is kept with --separate-git-dir, in a submodule, on git < 2.36, and when it holds local-agent/.
… config (Tencent#993) A repository created with an empty template has no .git/info/. The writability check ran `access(W_OK)` on that directory, got ENOENT, and reported it as "not writable", so every team MCP server holding a resolved value was withheld. Check the exclude file when it exists, otherwise its closest existing directory, and let the write create info/. Report "not writable" only for EACCES, EPERM and EROFS, and name the exclude file in the reason and the fix. Applies to pull, doctor, `mcp list` and local-agent installs, which share ensureExcludedFromGit.
…ads (Tencent#993) CodeBuddy reads only the first user MCP file that exists of ~/.codebuddy/.mcp.json, ~/.codebuddy/mcp.json and ~/.codebuddy.json, and teamai always wrote the second, so team servers and the member's own hid each other. In user scope teamai now writes to that first existing file and creates ~/.codebuddy/.mcp.json only when none exists. Each CodeBuddy record in managed-mcp.json names its file; a pull moves teamai's own entries out of a file CodeBuddy no longer reads, uninstall and `teamai mcp remove` follow the record, and doctor and `teamai mcp list` name a shadowed file.
All previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation. |
… syncing back (Tencent#993) A full uninstall that could not remove a tool's hooks kept the data directory with every tool still enabled, so the hook left in place ran a session-start pull that restored what had just been removed. Each tool whose hooks remain is now excluded, and the session-start pull of an excluded tool's hook does nothing, as its instruction and HTTP handlers already did, until the uninstall is run again after the repair.
The previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation. |
All previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation. |
The earlier |
The other previously reported findings are resolved. The PR description includes sufficient representative real-CLI validation. |
…Tencent#993) A full user-scope uninstall removed agent hooks, plugins and ~/.teamai without the local-agent lock, so a hook sync paused after loading its config could reinstall resources once uninstall reported success. Uninstall now runs the remove-http shutdown first: it waits up to 30 seconds for the lock, disables the source before teardown, and exits 1 without removing anything when the lock cannot be taken.
All previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation. |
…encent#993) A sync holds the HTTP source lock while it runs an uninstall_teamai command, so the uninstall it spawns waited for that lock until the hook's 15-second budget killed the sync, unacknowledged and with teamai still installed. The sync now passes its pid to the command; a child whose parent holds the lock runs under it without acquiring or releasing it.
|
No findings.
|
Conflicts resolved:
- uninstall: the base's incomplete-uninstall handling (hooksLeft, exit 1,
exclusions) moved into removeConfirmed; executeRemoval keeps heldMcp and
returns { pendingOpencode, hooksLeft }. An incomplete uninstall keeps the
exclude line of every file still on disk, for the retry to remove.
- local-agent: the exclude blocks and workspace models go before the base's
finishAgentHookTeardown, which keeps the disabled marker.
- pull: a linked docs root counts as the docs writer succeeding with no files.
- hooks, docs: union of both sides.
Also: the Codex dispatcher writer reads through readHookFile, the OpenCode
V2 judge uses claimTargets, an emptied OpenCode file's snapshot is keyed by
real path, and co-author's local settings check reads with readJsonObject.
Conflicts resolved: - local-agent: removeLocalAgentHttpLocked keeps the exclude block and workspace model removal before finishAgentHookTeardown(retry), whose outcome it returns. - uninstall: the HTTP source shutdown goes into removeConfirmed after the model gate, before the MCP cleanup (user scope only, so the project's recorded exclude files are still read at plan time); the home-only branch shuts the source down in place of teardownPlugins.
Resolve the docs reorganization (Tencent#1009): carry the Tencent#993 edits of usage-guide into the docs/guide pages that now hold each paragraph, apply the CI E2E setup change to docs/dev/ci-e2e-setup.md, and take main's deletion of the zh-CN team-secrets design.
The Auto-sync section moved to member-guide in the docs reorganization, so the in-page anchors Tencent#993 added no longer resolve.
|
No findings.
|
Brings in origin/main's docs reorganization (Tencent#1009) through Tencent#995's merge. The Tencent#915 edits of usage-guide move to the docs/guide pages that now hold each paragraph; the new "Keeping Delivered Files Out of Git" section goes to the end of member-guide, after co-author attribution as before, and in-page links to sections on other pages now name the page.
|
No findings.
|
Tencent#995 landed on main as d89c51e, a squash with the same content as its branch tip b0c36d2, which this branch already contains; the merge takes main as a parent and changes no file.
…915) (#1000) * test(agents): record the stale Kiro .md sibling as teamai's in the fixture (#993) * fix(docs): keep a member's entry whose type conflicts with a team doc (#993) The mirror replaced a local directory at a team doc file's path, or a local file at a team docs directory's path, and deleted the backup with its staging directory. membersDocs now treats such an entry as the member's unless the team history proves it teamai's (a file where the team now has a directory, or a directory whose every file is a team version), and copyDocs leaves a kept entry in place. A link is still replaced without touching its target. * test(skills): expect the not-teamai's line for a member's copy of a removed skill (#993) * refactor(git-exclude): one module owns git exclude blocks, mcp-exclude moves onto it (#915) src/git-exclude.ts owns info/exclude files: sync (replace), ensure (add-only gate with a per-path result), remove (owner or all, with a keep predicate) and report. Owners are named [a-z0-9/_%-] with per-owner markers; mcp-exclude keeps #886's markers, trimmed parsing and its already-ignored rule, and ensureExcludedFromGit maps ensure's results to GitExclusion unchanged. Lines route to the repository a path lands in (submodule, nested clone, symlink target), use the on-disk spelling, NFC only with core.precomposeunicode, and refuse line breaks and trailing spaces. Each owner's exclude files live in a caller-supplied record; stateHomeRecord keeps them in <stateHome>/git-exclude.json. git children drop the repository variables a hook exports. The re-including rule's source is now resolved from the toplevel, where git names it, not from the file's directory: a rule in .claude/.gitignore was named as .claude/rules/.claude/.gitignore. * fix(docs): keep a member's link at a team doc's path, without following it (#993) membersDocs skipped links, so the mirror still moved a member's link aside and deleted it with the staging directory (rest of the Codex P1). A link at a doc's path, or at an ancestor of one, is now the member's unless the team has the same link there: it is kept, named and never followed, and the docs under it are not delivered. * ci: re-run checks after an unrelated recall-attribution flake (#993) Node 22 on macOS failed on 9309e201 with one unhandled rejection from src/__tests__/recall-attribution.test.ts (a votes-lock write outliving the test's temp HOME); every test passed and #995 does not touch that code. The fork cannot re-run jobs, so this empty commit re-triggers CI. * fix(git-exclude): never take an unreadable exclude file for empty (#915) updateFileLocked read through readFileSafe, which turns every read error into empty content: an info/exclude that exists but cannot be read was rewritten with only teamai's block, dropping the member's lines, and the ensure that gated a resolved MCP value reported success. A read failure other than ENOENT/ENOTDIR now throws NotReadableError and writes nothing. ensure returns notReadable {path, error, reason, fix}; sync and remove report write kind notReadable and keep the file in the owner's record; ensureExcludedFromGit maps it to failed, so the secret is withheld. A missing exclude file is still created. * feat(git-exclude): keep delivered skills out of git behind sharing.gitExclude (#915) sharing.gitExclude.enabled (default false; init writes true into a new git-mode or single-repo teamai.yaml) and the partition override gitExcludeEnabled resolve as override, team, false. pull() carries one DeliveryRecorder for the project scope. Writers report landed paths per writer id and say whether they succeeded or failed; the skills handler is the first (through the pull's ledger, so the local agent's ledger-less calls report nothing). After pullSources, still under the partition sync lock and skipped for a contended scope, pull merges the reports into the checkout record's gitExcludePaths (fast path add-only; full sync replaces writer by writer, a failed writer keeps its previous entries still on disk) and syncs teamai's delivered block in the clone's info/exclude, or removes only that block when the flag is off. The list is kept whatever the flag; awaitingFullSync and the complete-branch rebuild carry it, and a record without it misses the fast path, so the first pull after upgrading from 0.26.0 is a full sync. The delivered owner's exclude files are recorded in the partition state (gitExcludeFiles). * feat(git-exclude): keep everything a pull delivers out of git (#915) Every writer of a project-scope pull now reports what it wrote, or confirmed as teamai's, to the delivery recorder, and says whether it delivered all it meant to: - rules, one file per line (namespace subdirectories, flattened names, Copilot instructions), also from the fast path's rewrites; - agents, plus a copy a tool holds for its model while the record still matches it; - the built-in teamai skill and the teamai-recall rule and agent; - the owned teamai-context files that hold teamai's blocks; - the main checkout's .claude/settings.local.json while its hook manifest records team hooks there, and Copilot's .github/hooks/teamai.json while it holds teamai's entries, read from disk so an unresolved team hook set or Copilot's built-ins-installed skip still lists them; - .claude/settings.local.json while the co-author record says teamai wrote the empty trailer and the file still holds it; - source skills this pull landed, never the manifest's entries. No configured source is a successful empty result (the copies stay on disk, visible); a source left as it was keeps its previous lines. A copy pull keeps for the member's edit is never reported, so it leaves the block. Collisions and failed writes mark their writer failed. The acceptance fixture (roles, a project, a source, recall, team hooks, MCP with and without a resolved value, Copilot plus six agents, a Codex copy in .agents/skills, a tracked SKILL.md with a new team file) checks git status against an allowlist of paths later tickets move, after init, pull and a session start, with the member's files visible and addable. * ci: re-run the Codex PR review, cancelled at its time limit (#993) * fix(mcp): save the manifest when an adoption records a server it did not rewrite (#993) An unrecorded server equal to today's team render was adopted into the in-memory records, but with no file write the manifest was not saved, so a later team removal or uninstall found nothing owned and left the server installed. The reconcile now saves the manifest whenever this run changed it. * fix(mcp): preview a no-op MCP adoption on --dry-run, and cover uninstall (#993) A dry run now says it would record an unrecorded server that already holds the team's render, in .mcp.json-style files and in Codex config, and still writes nothing. The real-CLI cases add the dry run before the adoption and an adoption followed by teamai uninstall; both failed before b62e503b. Hook adoption needs no change: reconcileHooks saves its records whenever they differ from the previous ones, whether or not it wrote a file. * feat(git-exclude): give Copilot the team instructions from a file teamai owns (#915) With sharing.gitExclude on, Copilot's project culture, shared-instructions and recall blocks go to .github/instructions/teamai-context.instructions.md with applyTo: "**", which the delivered git exclude block lists. The next pull strips teamai's blocks from .github/copilot-instructions.md and deletes it only when teamai created it and git does not track it. Turning the flag off moves the blocks back on the next pull. Doctor checks the new file through the existing instruction checks; the usage guide names the Copilot surfaces that read no .github/instructions file and the chat.includeApplyingInstructions dependency. * fix(docs): keep a member's directory where the team removed a doc file (#993) When the team deleted a doc file the member had replaced with a directory, the prune walked into that directory and deleted its files, which match no team version. A directory at a path where the team history had a doc file, holding anything that is not a team version, is now kept whole and named. * feat(git-exclude): keep tracked copies a removal would delete, and sweep Codex's shared skills (#915) When a delivered copy stops being delivered (role or project switch, a root skill once roles are set, a team tombstone or removed rule, a source dropping a skill), the removal pass now keeps a path the checkout's index tracks and names it once per pull with the git rm -r hint, so a later pass that proves the copy teamai's still leaves it. The inactive-namespace, stale-skill and tombstone sweeps also visit Codex's .agents/skills/<name>, deleting a copy only when judgeRemoval returns remove and naming an edited or member copy. * feat(git-exclude): keep self-mode team hooks out of the tracked settings (#915) In a single-repo team with sharing.gitExclude on, Claude Code's team hooks go to each checkout's own .claude/settings.local.json, which the delivered block lists, and the committed .claude/settings.json keeps only the built-ins, so a fresh clone still has them and a pull leaves git status clean. The per-checkout hook index records the team hooks of the one file that holds them. Turned off, the next pull moves them back into the tracked settings. init . syncs the delivered block after its own writers, since it ends without a pull. hooks remove and uninstall also clear settings.local.json. * fix(mcp): remove an unrecorded copy of a server the team deleted (#993) With a lost manifest, a server an older release installed stayed for good once the team deleted it: only today's names were judged, and with no team server and nothing owned the reconcile returned early. Pull and uninstall now also remove an unrecorded entry, in .mcp.json-style files and Codex config, that equals a render of that server from the team history. A member's entry under that name stays. The early return now also requires a team history with no MCP server. * feat(git-exclude): one delivered block for every live checkout, and other repositories' paths where git reads them (#915) A pull now lists in teamai's delivered git exclude blocks the union of the lists of the project's live checkouts (liveCheckoutRecords, now exported), so a pull in one worktree no longer drops another worktree's lines, also in a --separate-git-dir repo and in a project that is a submodule. A removed or pruned worktree's lines go on the next pull, fast path included. A delivered path that holds a file of the member's in another live checkout (not in that checkout's list) gets no line, since the shared line would hide that file too; pull names it. A checkout with no list yet has no such files, and the member's own .claude/settings.local.json in a linked worktree is never one. A path tracked in one checkout and untracked in another stays listed. Paths that land in another repository (a tool folder that is a submodule or a nested clone, a tool home under version control) go to that repository's exclude file, in a delivered/<id> block named after the partition, so the superproject stops showing the submodule modified and projects sharing a tool home each manage only their own block. Turned off, both blocks go. * perf(sync): read the team's MCP history once per pull, in one git call (#993) historicalContents read each historical blob with its own git process (about 370 ms for 60 versions); it now reads them all with one git cat-file --batch (about 30 ms). The MCP reconcile reads that history once per run for every target, and only when a target file holds an unrecorded entry; with no team server, nothing recorded and no server in any target file it returns without reading it. A dry run names an unrecorded copy of a server the team deleted that a pull would remove. * fix(docs): keep a member's link where the team deleted a doc (#993) The prune unlinked any link at a removed team doc's path. A link there is now removed only when it is a version of that doc from the team history (git stores a link as a blob of its target); a link of the member's stays, and is never followed. A link at a path the team never had is pruned as before. * fix(coauthor): keep the record of a shared settings file that does not parse (#993) A pull that could not read a recorded shared settings file dropped its record, so once the member repaired the file no later pull removed the old attribution value. The record now stays until the file can be read. * fix(docs): keep any non-file entry of the member's where the team deleted a doc (#993) A link at a removed team doc's path goes only when it is a version of that doc from the team history; any other entry that is not a regular file stays and is named. Adds the real-CLI case for a member's link replacing a doc. * fix(sync): never adopt or write through a link at a delivered file's path (#993) isTeamaiCopy and the copy judgement followed links: a link at a rule or agent path whose target held an older team version was taken as teamai's, and the next pull wrote through it into the member's file. A link in place of a delivered file is now the member's (kept, named), a link there is never removed as teamai's copy, and a skill directory holding a link is not teamai's copy, since copying over it would write through it. * fix(docs): keep a member's directory of links where the team deleted a doc file, and do not call it stale (#993) A directory that replaced a former team doc file and held only links read as teamai's (links are not listed), so the prune removed it. Such a directory is now the member's whenever it holds a link. doctor also stops listing files inside a directory pull keeps whole as stale docs. * fix(hooks): recognize unrecorded team hooks in every format, on pull and uninstall (#993) Pull recovered a team hook entry whose record was lost only in Claude and Codex files. In Cursor, Copilot and ZCode files the old entry stayed as the member's and the new one was added beside it, so both ran. Each entry is now compared with the team's hooks rendered the way that format's reconciler writes them; one equal to exactly one hook is replaced or removed as teamai's. Uninstall did not judge unrecorded entries at all, so with the records lost it left custom team hooks installed, and missed a main-checkout Codex file holding only team hooks. It now judges every hook file against the team's hook history, gated to the project as pull renders them in a non-self project scope, and removes the entries equal to exactly one team hook. * fix(sync): treat any non-regular entry in a skill or docs directory as the member's (#993) A directory is teamai's copy only when every entry is a regular file that is a team version; a link or any other non-regular entry anywhere below it makes it the member's. Adds real-CLI cases for a skill directory holding a link and a docs directory of links where the team deleted a doc file. * fix(sync): keep a member's link at any delivered path (#993) A link at a delivered skill directory, rule or agent file, docs entry or source skill destination is the member's: teamai never creates links, so it is never adopted, replaced, written through or deleted, on pull, remove and uninstall alike. Pull and doctor name it: "Kept <path>: it is a link of yours, so teamai does not replace it. Remove the link to receive <name> from the team." This deliberately changes earlier behavior: source and team skills used to replace a leaf link in place of a skill directory with a copy, and the Codex duplicate cleanup deleted a configured leaf link. The source tests that asserted those now expect the link kept. * fix(git-exclude): report an unreadable exclude file as such, and ask git a fixed number of times per checkout (#915) * fix(agents): prove a tombstoned agent copy teamai's in a team-defined tool too (#993) For a tool teamai renders no agents for, the tombstone cleanup had no proof to check and removed any file with the removed agent's name. It now removes it only when it is a team agent's own bytes from the history; a member's file of that name stays. * fix(rules): never read through, write through or delete a member's link at a rule path (#993) The cleanup and rerender paths judged a rule copy by reading the file a link points at, so a link whose target held a team render was rewritten through or deleted: the "Already synced" rerender, the legacy rules directories (pull and uninstall), the reserved teamai-context name, the unselected-rule sweep, the moved nested copy, the flat OMP and Kiro copies, the older .md layout, and teamai remove rules. Each now treats a link as the member's and leaves it. The empty-directory sweep no longer deletes a directory whose only entries are links. * fix(agents): keep a member's link where an agent was delivered, on pull and teamai remove (#993) The inactive-namespace cleanup judged a copy by the file a link points at, and teamai remove agents deleted the author's root copy by its placement record alone. A link at either path is now the member's and stays. * fix(skills): keep a skill directory that holds a member's link, on pull, remove and uninstall (#993) A recorded skill whose file the member replaced with a link to the same bytes read as unchanged, so pull copied over the link and remove, uninstall and the tombstone and inactive-namespace sweeps deleted the directory with every link in it. A skill directory holding a link is now the member's on pull, named by the link, and kept by every sweep. teamai remove skills also judges a link at a built-in skill's name before the name. * fix(sync): keep an unrecorded file teamai cannot read where a removed resource was delivered (#993) judgeRemoval counted a file it could not read as absent and removed it. Without its bytes nothing proves it teamai's, so it is now kept as the member's. * fix(docs): never read through or delete inside a member's link in a deactivated docs namespace (#993) Withdrawing a namespace's docs read each copy through any link on its path and deleted what matched the team file. A namespace directory the member linked to a folder of their own lost its files, and a linked doc was deleted. A path through a link is now left alone. * fix(source): keep a source skill copy that holds a member's link, on pull and source remove (#993) A recorded destination was the source's whole, so pull copied over a link the member put inside it and source remove deleted the directory with it. Such a copy is now the member's: pull names the link and source remove keeps the directory on record. * fix(local-agent): keep a member's link where the local agent installs a skill or rule (#993) The local agent installs through pullItem with no delivery record, which skipped every ownership check: a rule was written through a link and a skill directory link was replaced. A link at the path, or inside the skill directory, is now kept and named. * fix(sync): deliver team and source skills without the links in them (#993) Copying a skill recreated each link in its source as a link in the member's tool directory, which then read as the member's own, and let a skill plant a link to any target. Delivery now skips a link in a team or source skill, without following it, and names each one once: "Skipped <link> in <skill>: teamai does not deliver links." * fix(docs): keep a member's link in the docs mirror at a path the team never had (#993) This deliberately changes the mirror prune: it used to unlink any link at a path the team repo's history never had, as a local leftover. teamai never creates a link there, so such a link is the member's: pull now keeps it and names it, and doctor no longer reports it as a stale doc. A link matching a team link once at a removed doc's path is still pruned. The docs and doctor tests that expected the unlink now expect it kept. * feat(git-exclude): doctor checks the delivered blocks, pull --dry-run previews them, background pulls keep what they could not say (#915) * docs(designs): state that teamai never follows, replaces or deletes a member's link (#993) * fix(uninstall): remove only teamai's docs from the docs mirror, and keep the member's (#993) Uninstall deleted the whole docs directory, so a file pull had kept as the member's, a directory or a link of theirs went with it. It now removes a file or link only when it is a version of that doc from the team repo's history, names everything else, and removes a directory only once it is empty. While the history cannot be read, the mirror stays whole. A mirror inside the data home that keeps something survives the removal of that home with it. * feat(removals): never delete a file git tracks, in layout migrations and explicit commands (#915) Layout migrations (a rule's legacy .md and superseded copy, legacy rule directories, moved nested copies, agent format siblings, the Codex configured-copy dedupe, leftover files of another skill version) keep a tracked old copy and say where the resource lives now. teamai remove, source remove and uninstall remove the rest and name each tracked path; uninstall's summary lists them under Kept (tracked). A team repo with no rules left now runs the rules sweep, so the copies of a last rule the team deleted go, on the same ownership proof. Test names and comments describe behaviour instead of how the work was split. * test(commands-reference): load the command table in a fresh module registry (#915) The test now imports the CLI entry after vi.resetModules() and restores the env stub after it, so it reads the table under its own guard whatever another file in the same worker loaded or stubbed. * fix(mcp): write a linked MCP config through its link, and keep its target out of git (#993) A member's MCP config can be a symlink into a dotfiles repository. The atomic writes replaced the link with a regular file. They now write at the file the link points to, rollbacks included, and a linked CodeBuddy ~/.codebuddy/mcp.json is kept rather than deleted. A resolved value then lands in the link's target, so the git checks judge that file in its own repository: a target that repository tracks gets no value, and an untracked one is listed in that repository's info/exclude, which pull and uninstall now find to take the line out again. * fix(docs): name a kept link at a removed doc's path as before, and only a never-team path's link as such (#993) Goes with keeping a member's link at a path the team never had: a link at a removed doc's path keeps the line that says the team removed it. * test(skills): expect pull to name the member's link inside a skill directory (#993) * feat(git-exclude): doctor notes the copies pull keeps because the repository tracks them; document the checks, the preview and background pulls (#915) * fix(opencode): keep a member's linked opencode.json and openclaw.json as links (#993) The OpenCode instructions writer and the OpenClaw hook entry writer replaced a symlinked config with a regular file, and uninstall deleted a linked .opencode/opencode.json left holding only its $schema. Both now write at the link's target, and a link is never deleted. * fix(rules): never delete a linked CLAUDE.md or AGENTS.md when its teamai section goes (#993) Removing teamai's section from an instruction file teamai created deleted the file once nothing else was left, a member's link to it included. A link stays; its target is emptied instead. * fix(skills): push no links to the team repo, and name why remove and uninstall keep a skill directory (#993) A push copied a link inside the member's skill into the team repo, so every member then received a skill teamai will not deliver; push now skips each link and names it. remove and uninstall now say a kept skill directory is a link of the member's, or holds one, instead of claiming it has no delivery record. * fix(git-exclude): report a delivered path a rule re-includes as a failed sync (#915) * fix(source): keep reading a record into a removed OpenClaw workspace, and never another repository's history (#993) A source record of a copy in an OpenClaw workspace outside ~/.openclaw was rejected once that workspace was gone, so every pull and source remove of that source failed. The configured workspace now counts as a tool home even when its directory is missing. A source cache without its own .git no longer lets git read an enclosing repository's history (a dotfiles HOME): only today's source proves a copy then. * feat(git-exclude): a checkout on the un-migrated layout does not decide the setting with its own gitExcludeEnabled (#915) * feat(removals): keep tracked files in the built-in skill prune, and cover every tracked-copy guard through the real CLI (#915) * fix(source): leave a copy in a tool home that moved, instead of rejecting the record (#993) Configuring OpenClaw to another workspace made the record of a copy in the old one invalid, so every pull and source remove of that source failed. The record now notes the tool home of each absolute destination; one whose home moved is dropped from the record and its copy left as it is, while an absolute path the record cannot account for is still rejected. * fix(docs): keep the member's hidden files on uninstall, and match a link only to a team link (#993) uninstall did not count hidden entries in the docs mirror as kept, so with nothing else kept it deleted the data home around them. They are now named and keep the mirror. A link in the mirror is now teamai's only when the team history had a link with that target there, not a file holding the same text (the history now carries each version's git mode). * feat(uninstall): remove every teamai git exclude block after the files it hid; --agent drops only that tool's lines (#915) Uninstall reads the exclude files the project's delivered owners recorded, plus the project's own and the MCP ones, before it deletes anything, and under the partition's sync lock removes every teamai block there once the files are gone and before the partition state goes. Another project's delivered block in a shared repository stays. An MCP line stays while a config in any checkout of the repository may hold a resolved value, the main checkout of a --separate-git-dir repo included; a credentials line stays while its file is there. A read-only exclude file is named with the lines to delete by hand; a repository that is gone is skipped. uninstall --agent drops the tool's paths from every checkout's list, keeping a path another tool in use reads, and syncs the blocks again. uninstall --dry-run lists the blocks by owner and file. HTTP mode: a skill the local agent installed under its SKILL.md name (dir_name in its manifest) is now removed too. * fix(git-exclude): keep a recorded block when a path cannot be placed (#915) When git cannot say which repository a path lands in (sync reports it in gitFailed), its line may sit in any exclude file the owner recorded. A replace in that run emptied such a block and dropped the file from the owner's record, removing protection while reporting the failure. While any path fails placement, sync now only adds: every block keeps its lines, every recorded file stays recorded, and the paths that placed are still listed. The next sync in which every path places replaces as before. * fix(git-exclude): treat an unverified exclusion as a failure (#915) After writing a path's line, ensure counted every answer of `git check-ignore` except "would commit" as excluded. A check that timed out or failed in any other way therefore let the MCP reconcile write a resolved value into a file git was never shown to ignore. Only a verified "ignored" now counts as excluded; any other answer is gitFailed, naming git's error and the check to repair. The exclude file stays recorded, as its line was written. ensureExcludedFromGit maps this to its failed result, so the value is withheld. Two MCP exclude tests asserted the old reading and now expect the failure: git failing check-ignore while ls-files answers, and a path beyond a dangling directory symlink, which git refuses to check. * fix(skills): keep a delivered skill directory that holds a file the member added (#993) A skill directory with any file on record was taken as teamai's whole, so remove, uninstall and the tombstone cleanup deleted a file the member had added beside teamai's. A recorded directory now goes only when every file in it is on record or a team version; otherwise it is kept whole and named. Source skill removal applies the same rule against the source's files and history. * docs(mcp): a resolved value waits for git to confirm the exclusion (#915) * fix(skills): remove and uninstall take only teamai's files from a skill directory (#993) When a delivered skill directory holds a file the member added, remove and uninstall now delete only the files on record or matching a team version, name each file they keep, and leave the directory while anything is left in it. uninstall plans the exact files, so its dry run shows them. * fix(source): keep a member's file at the path of a link the source skill has (#993) Delivery never copies a link from a source skill, but removal counted any history at that path as the source's, the link included, so a regular file the member put there was deleted with the skill. Only a regular file the source has, or had, is the source's now. * fix(mcp): find teamai's CodeBuddy servers in every user MCP file, and treat a linked lookup path as its target (#993) A server an earlier teamai left in a CodeBuddy user MCP file that CodeBuddy no longer reads stayed there once its record was lost: pull and uninstall looked only at the file CodeBuddy reads, and returned early when that file was empty. They now also look at the other lookup files that exist and remove an unrecorded entry equal to a current or historical team render; a member's entry stays. Lookup files are compared by real path. With ~/.codebuddy/.mcp.json a symlink to ~/.codebuddy/mcp.json, a pull recorded the server at the link and then removed it from its target, the same file. With ~/.codebuddy.json a symlink to ~/.codebuddy/mcp.json, the upgrade move emptied and deleted the file the link points to. * fix(git-exclude): name the check to run when git cannot confirm an exclusion (#915) When git cannot confirm that it ignores a file after teamai lists it, the value stays withheld. The message now quotes git's error and names the next step: check that `git check-ignore -v <path>` works in that repository, then run the command again. The two MCP exclude tests changed in the previous fix asserted that an unconfirmed exclusion still let the resolved value be written, which is the behaviour fixed there; one of them now asserts the full warning. * fix(git-exclude): keep both delivered blocks when a path's repository cannot be located (#915) Pull splits delivered paths between the project's `delivered` block and `delivered/<id>` by the repository git names for each. A path whose repository git could not locate (a tool folder whose `.git` points to a deleted git directory) went to `delivered/<id>` only, so the `delivered` sync replaced its block and dropped the path's line, although git still reads that line from the project's own exclude file. Such a path now goes to both syncs. Each reports it as not placed and keeps every line its block holds; pull warns once per path. * fix(rules): keep a tracked teamai-context rule copy where nothing rewrites that path (#915) An earlier release's copy of a team rule named teamai-context is reclaimed at the tool's instruction file, which the instruction sync rewrites right after (Claude, Cursor, CodeBuddy, WorkBuddy). For OpenCode's .opencode/rules and Kiro's steering directory nothing rewrites it, so a copy the business repository tracks is now kept and named, as every other removal keeps one. * fix(uninstall): name a built-in skill file kept only because it is tracked once, without the member-files warning (#915) When removeOwnedFiles keeps a packaged file only because the repository tracks it, keepsTrackedCopy already names it. Uninstall no longer adds "holds files TeamAI did not put there. The packaged files were removed" for that directory, as pruneLegacyBuiltinSkills and retireOtherCodexCopy already do. * feat(mcp): MCP cleanup and the pull-time release reach every checkout of the project (#915) projectWorktreeConfigs and the pull-time MCP git exclude release now enumerate checkouts with projectCheckouts (live recorded checkouts plus a complete git worktree list, every recorded root when git cannot list them all), moved next to liveCheckoutRecords. From a linked worktree of a --separate-git-dir repo or a submodule, uninstall now removes teamai's servers and their resolved values from the main checkout's MCP config too, and drops the line once it is clean. A pull there no longer releases the shared .mcp.json line while the main checkout's config still holds a resolved value; that checkout's own pull cleans it and releases the line. Also covers uninstall --agent codebuddy and --agent workbuddy keeping the .codebuddy/rules lines the other tool still reads. * fix(sync): prove a CONTRIBUTORS file like any other, and a file only by a file the team had (#993) A skill's CONTRIBUTORS file counted as teamai's by its name alone, so remove and uninstall deleted the member's own one, and it never made a directory the member's. It now needs the same proof as any file. A link in the team history no longer proves a regular file holding the same bytes, and a version that changed only its mode is kept as its own version. * fix(skills): keep a delivered skill when the team or the source adds a file where the member has one (#993) A file the member added to a recorded team or source skill was overwritten once the team, or the source, delivered a file at that path. Pull now keeps such a copy whole and names it, as an edited one, unless the file is a version from the history. A source removal keeps a copy holding a file of the member's only when the source's own files prove the copy came from it: a copy from a replaced repository, or one an in-repo alias placed, is left to the record, as before. * fix(docs): keep a file of the member's at a path the team never had (#993) The docs mirror pruned any local file at a path the team history never had, so a member's own notes there were deleted on the next pull. Having no team version is no proof that teamai put a file there: the prune now removes only teamai's copies of docs the team removed, and leaves the member's own files without a word. doctor stops calling them stale, and the usage guide no longer says local drafts are removed. Earlier tests that expected such files pruned now use team docs the team removed. * docs(designs): the docs mirror prunes only teamai's copies of removed docs (#993) * fix(git-exclude): keep the blocks when the team setting cannot be read (#915) With the team's teamai.yaml missing or invalid and no member override, sharing.gitExclude fell back to off, so pull removed the delivered blocks and git add -A staged every delivered resource. In git mode that case is now unknown: pull and its preview leave the blocks as they are and report a failed sync (kept by a background pull), and doctor fails with the same message and names the source "team config unreadable". A member override still decides, and HTTP mode, which has no team setting, is unchanged. * feat(local-agent): keep its project skills and rules out of git, and never overwrite a member's file (#915) The skills and rules the HTTP local agent installs in a project now go in a local-agent block of the exclude file of the repository each copy lands in, while that project's git exclude flag is on (the project's config.yaml, else ~/.teamai/config.yaml). The agent syncs the block after each batch of project installs and uninstalls, lists only workspaces that are live checkouts, and records the exclude files it wrote in ~/.teamai/local-agent/git-exclude.json. Manifest entries record the tools they were written to; an entry from an older CLI counts as recorded where its copy equals the installed version or sits under its dir_name, and gains the field on its next install. A skill or rule destination the manifest does not record is the member's: the install is not done there and fails with "Kept <path>: it is not teamai's (not in the local agent's records). Rename or delete it; the local agent installs <slug> on its next sync." A copy equal to the download is taken over. source remove-http drops the block after uninstalling each resource, also when one fails, and teamai uninstall removes it from every exclude file the agent recorded, another repository's included. The instruction targets read the same per-workspace flag. The HTTP test backend gains rule downloads, the get-config, plugins/config and projects/mine routes, and scope/workspace_path on every command. * fix(rules): remove a rule copy that holds what pull writes today (#993) `remove rules` deleted a tool's copy only on the checkout's record or the team history. A checkout without history and without a record, such as a resource cache, left teamai's own copy behind. The copy that holds today's render of the team rule, or the rule verbatim, is now teamai's too, read before the team file goes, as `remove skills` already does. * fix(source): keep a source skill copy holding the member's file, on record (#993) A copy pull kept whole because the source added a file where the member had one was left off `installedSkills`, so `source remove` forgot it and dropped the manifest. It stays on record now. Removal then tells the source's files from the member's by their bytes, not their path, and a file at a path the skill never had keeps the directory even when no file of the source is left in it. * test(local-agent): uninstall_rule removes the installed copy and keeps the member's (#993) * feat(local-agent): keep model API keys in .codebuddy/models.json out of git, whatever the flag (#915) Before the local agent writes a model API key into a project's .codebuddy/models.json, it lists the file in the `credentials` block of the repository's .git/info/exclude and writes the key only once git confirms it ignores the file. A tracked file, one a .gitignore rule re-includes, an exclude file it cannot write, a workspace outside a git repository or an exclusion git cannot confirm leaves the file as it was; the task fails with the reason and the fix. The block's exclude files are recorded in ~/.teamai/local-agent/git-exclude.json. teamai no longer creates .codebuddy/.gitignore, and deletes the one it created while it holds only its two lines. An apply_model_config with no models, and `source remove-http`, delete a models file left with nothing (never a tracked one or a link), then its line. `teamai uninstall` keeps a credentials line, with a warning, while the models file still holds a key in any checkout of the repository. * fix(local-agent): write the model key outside a repository, and delete only models files teamai created (#915) A workspace outside any git repository has nothing that could commit .codebuddy/models.json, so the key is written there as before, with no git exclude line and no record of one. An empty model config, or removing the HTTP source, deletes the project models file only when teamai created it, recorded in the model manifest in the local agent's state home from now on. A file teamai did not create stays, and so does its line. * feat(local-agent): follow each project's git exclude option at every session start (#915) The local agent's `local-agent` block was only rebuilt after a batch that installed or removed a project skill or rule, so turning the option on or off in HTTP mode took effect only when the backend happened to send one. - Rebuild the block from the manifest and each workspace's option at every session start, and after project installs and uninstalls. The agent's other runs (prompt, tool call, stop) rebuild it only when the manifest or a config.yaml the option is read from changed since the last rebuild, judged by a hash kept in the local agent's state home, so they cost no git call (about 8 ms per prompt in a one-project measurement, against about 30 ms for an unconditional rebuild). - Keep a failed rebuild in the local agent's state home, under its own owner key, so a pull's success never clears it and the reverse. The next interactive pull prints it once, and doctor fails a check until a rebuild succeeds. Paths no line can name are kept as notices. - A workspace following a git-mode config whose team teamai.yaml cannot be read (and no gitExcludeEnabled) keeps the lines it has, and the agent records why; it installs and removes nothing there meanwhile. * fix(mcp): tell another tool's server by the file's real path (#993) An entry another tool's record claims in the same file is left to that tool. The file was compared by name, so a tool whose MCP path links to another's file adopted that tool's server once its own record was lost, and a later removal for it would take the server from the shared file. It is compared by real path now, as the rest of MCP reconciliation does. * fix(source): keep a source skill copy whose every file the member edited (#993) Removal kept a copy holding an edited file only when another file still matched the source. A copy whose every file the member edited, such as a single SKILL.md, was deleted by `source remove` or by a pull after the source withdrew the skill. When the source history has the skill, a file that matches no source version keeps the copy. * fix(local-agent): say what a refused install or uninstall left undone, and how to fix it (#915) While a project's flag cannot be resolved, the refusal now names the resource, the project and the unreadable file, says nothing was written, and gives both ways out: fix or restore the team's teamai.yaml, or set gitExcludeEnabled in the project's config.yaml. * fix(ownership): keep a member's entry of the other type at a delivered path (#993) A file where a team skill directory goes, or a directory where a rule or agent file goes, was not judged the member's. Delivery over it failed, yet the pull could record the team revision as synced, so once the member removed the entry a plain pull delivered nothing until the team changed or the member ran `pull --force`. Such an entry is now kept and named, and the next pull after it is gone delivers the team version. * feat(pull): keep shared config files that hold only teamai's entries out of git (#915) A project MCP config without a per-member place (.cursor/mcp.json, .github/mcp.json, .codex/config.toml, Kiro, OMP, Pi, .workbuddy/mcp.json, OpenCode's root opencode.json), .codex/hooks.json and OpenCode's .opencode/opencode.json are listed in the delivered block while git does not track them and they hold nothing but teamai's entries: no other top-level key ($schema included), and every server, team hook or instructions entry one teamai's records own (equal to what teamai writes, for the OpenCode instructions). They are judged after the MCP and hook reconciles, so a file written before the upgrade or after its records were lost qualifies by content. They are judged on every pull, fast path included, and replace their previous list (DeliveryRecorder.judgedAll). The first pull that finds an entry teamai does not own in a listed file takes its line out and says "<path> now holds entries teamai does not own, so git can see it."; a background pull keeps it as a git exclude notice for the next interactive pull. The usage guide documents the files and the limit when a teammate commits the same path. * fix(skills): keep a member's file when removing a skill with a built-in's name (#993) `teamai remove skills <name>` deleted the whole directory when the name is one the CLI once shipped (such as team-wiki-codebase), so a file the member added beside it was lost. It now removes the files the team history proves teamai's and the files a release packaged there, as uninstall does, and keeps and names a directory left holding anything else. * fix(skills): keep a delivered skill when the team adds a file where the member has a directory (#993) When the team added a file to a delivered skill at a path where the member had a directory, or a directory where the member had a file, the copy failed inside pull and the pull still advanced to the team revision, leaving the skill stale. Such an entry is the member's now: the skill is kept whole and named, as when the team adds a file where the member has one. * feat(docs): keep the delivered docs mirror out of git, still searchable (#915) * fix(pull): say nothing when a shared MCP config with a resolved value takes in a member's server (#915) Such a file stays in the mcp-exclude block whatever it holds, so leaving the delivered block does not make it visible, and "git can see it" was wrong. Also cover a .codex/hooks.json written before the upgrade (no hook records) and a single-repo team's .codex/hooks.json, which keeps the built-in hooks and stays visible. * fix(local-agent): tell CodeBuddy's user MCP file by its real path (#993) With `~/.codebuddy/.mcp.json` linked to `mcp.json`, a record an older agent wrote without a file resolved to `mcp.json`, while the install target was `.mcp.json`. The record was dropped as another file's, so reinstalling the same server failed as "not managed by teamai". Records are matched to the target by real path now, as MCP reconciliation does. * fix(skills): deliver a skill around a member's entry of the other type, and retry failed copies (#993) A member's directory where the team adds a file to a delivered skill, or a file where it adds a directory, is the member's: it is kept and named, the files it covers stay undelivered, and the rest of the skill is delivered. The pull is not counted as synced, so the next one is a full sync that delivers those files once the entry is gone. A skill, rule or agent copy that fails with an error no longer counts as synced either: the revision stays, and the next pull retries it. * fix(source): keep a source skill copy when the source adds a directory where the member has a file (#993) A file of the member's where the source now has a directory was missed by the collision check, so the copy failed on the conflict instead of keeping the member's copy. An entry of the other type at a source path, or at one of its parents, is the member's now, also when the source cache has no history. * fix(uninstall): keep OpenCode's user plugin on a project uninstall (#993) The plugin in the user's OpenCode config now also carries the team rules and instructions for V2, for every project and the user scope. A project-scope uninstall removed it, which ended delivery for every other project. It keeps it now and names it, as it does for the other global adapters; a user-scope uninstall still removes it, and so does `teamai hooks remove`. * fix(pull): keep a shared config's line while git cannot say it tracks the file (#915) A teamai-only file git could not answer for was judged `unknown`, and its entry was replaced with nothing, so its line left the block silently. Only a confirmed answer replaces the entry now; until then the line stays, the pull fails naming the file and git's error (a background pull keeps the failure), and doctor reports the checkout git cannot answer for. * fix(skills): keep a skill directory holding a git repository of the member's (#993) The ownership check reads a skill directory's files the way delivery lists them, which skips `.git`. A recorded skill whose files matched was then deleted whole by `teamai remove`, `uninstall` or a source removal, taking a repository the member made in it, with its local commits. A directory holding a `.git` is never deleted whole now; only teamai's files in it go. * fix(local-agent): keep a CodeBuddy server copy the member edited when moving it (#993) When CodeBuddy's preferred user MCP file changed, reinstalling a server moved it there and deleted the copy in the old file without checking it. A copy the member edited since teamai installed it is kept now, and named. * feat(mcp): give Claude the team's project MCP servers in its local scope (#915) With sharing.gitExclude on, Claude's project servers go to Claude Code's local scope, ~/.claude.json under projects[<key>].mcpServers, instead of .mcp.json. The key is the one Claude Code files the checkout under: the main checkout's real path for it and every linked worktree; in a --separate-git-dir repository a linked worktree's key is the git directory, and in a linked worktree of a submodule its directory under .git/modules. Resolved tokens leave the working tree, and Claude loads the servers without the approval prompt. Ownership is shared by every checkout using the key (managed-local-mcp.json in the project's data home), and uninstall removes exactly teamai's entries. The next pull moves teamai's servers out of .mcp.json, recorded or equal to a render in the team's history; keeps the member's own servers, a teamai server the member changed (named), and the names CodeBuddy still claims there; and deletes the file when nothing else is left and git does not track it. Its MCP exclude line goes once no checkout holds a resolved value. Turning the option off moves the servers back. While tclaude, which reads .mcp.json, is installed and enabled, Claude keeps the file. * feat(opencode): deliver team MCP servers through the plugin on OpenCode V2 (#915) With sharing.gitExclude on, OpenCode V2 and teamai's plugin current, a pull writes the project's team MCP servers to .opencode/teamai-mcp.json, which the plugin reads at setup and adds through ctx.mcp.transform for that project only. The file is teamai's whole, listed in the delivered block, and written only after the mcp-exclude block holds it when it carries a resolved value. The pull then takes teamai's V1 entries (recorded or adopted) out of the root opencode.json and .opencode/opencode.json when git does not track them and they hold nothing else, and deletes a file left empty. A tracked or mixed file is left; doctor names it. The plugin is reconciled before the decision, so a plugin from an older build is replaced and the V1 entries go in the same pull. Without a current plugin, on V1, or with the option off, the V1 entries stay or come back and the V2 file goes. The OpenCode major version is asked once per run. The plugin's project walk-up also stops at a .opencode/ holding only teamai-mcp.json. * fix(local-agent): keep a CodeBuddy server's record until its move is written (#993) Moving a server to the user MCP file CodeBuddy now reads recorded it in the new file before writing it there. When that write failed, the record pointed at the new file while the server stayed in the old one, so uninstall left it installed. The old file's record is existing ownership now: it stays until the new file is written, as for a server already recorded in the target file. * fix(uninstall): record a project's OpenCode exclusion when its plugins are kept (#993) A project-scope `uninstall --agent opencode` keeps the user plugin directory, so with only a server-pushed agent-hook plugin there the plan was empty: it said "Nothing to uninstall" and never excluded OpenCode, whose global hooks stayed active for the project. OpenCode now takes the exclusion-only path, like the other tools with global channels, and the kept agent-hook plugins are named with the other retained adapters. * fix(skills): list a skill delivered around a member's entry file by file (#915) A skill delivered around a member's entry of the other type was listed in the delivered exclude block as its whole directory, which hid the member's entry from git status. Such a skill is now listed one file per line, in pull and in the --dry-run preview. * fix(pull): list a delivered skill's files, never its directory (#915) A skill was listed as its directory (`/<dir>/`), so a file the member added inside a delivered skill was hidden from `git status` and `git add -A`. Every writer that delivers skills (team, role, project, source and built-in skills, Codex's `.agents/skills`, `.github/skills`, the local agent's installs) now reports the files it wrote, and the git exclude module refuses a directory. In a skill, a tracked file gets no line and is reported while the skill's other delivered files keep theirs; a member's negation that re-includes one of them fails the sync naming the rule. Doctor and `pull --dry-run` report per file. An older directory line goes on the next full sync, which rewrites the block. * feat(hooks): run Codex team hooks from a dispatcher when the project's hooks.json is not teamai's alone (#915) While sharing.gitExclude is on and a project's .codex/hooks.json is tracked, holds a member's entry or another top-level key (always in single-repo mode, whose committed file holds the built-ins), teamai stops writing the team's Codex hooks into it. The pull that sees this takes teamai's entries, recorded or adopted, out of the file through its hook manifest and records the project's hooks in ~/.teamai/codex-team-hooks.json; ~/.codex/hooks.json gets one `teamai hook-dispatch <Event> --tool codex --team-hooks` entry per event, timed out at the largest team-hook timeout, before the built-ins. Once the file is gone or teamai's alone again, the next pull writes the hooks back; turning the setting off does too. The new --team-hooks option of the hidden hook-dispatch command resolves the project from the hook's cwd, evaluates matchers, applies each hook's timeout (stopping its process group), and passes on stdout and a blocking exit 2. It exits at once when no project dispatches. The entries name no project, so their Codex trust holds as worktrees come and go; teamai trusts them like the built-ins, in single-repo mode too. Uninstall drops the project and the entries no other project needs. * fix(uninstall): keep the local agent's lines for the workspaces it still serves (#915) A project `teamai uninstall` removed every `local-agent` block the agent recorded, so another project's installs, and a linked worktree's in the shared exclude file, showed in `git status` until a later session start. The agent stays after a project uninstall, so its blocks are now rebuilt from its records without that project's workspace: only this project's lines go. A user-scope uninstall, which removes the agent, still removes every block. * fix(docs): keep a member's doc that has the bytes of a team doc that is a link (#993) A regular file of the member's at the path of a team doc that is a link was taken for teamai's copy when its bytes matched the link's target, and the mirror replaced it with the link. Equal bytes now prove a copy only of a team file; such a doc is the member's unless the team history has that file. * fix(mcp): keep a resolved value's own line when only teamai's blocks ignore the file (#915) A project MCP config that git already ignored got no `mcp-exclude` line. Since the `delivered` block lists teamai-only configs, that ignore could be teamai's own: once the member added a server, turned the option off or ran `uninstall --agent`, the `delivered` line went, and the file holding the resolved value became visible to `git add -A`. An ignore from teamai's own blocks no longer counts as already ignored: the line is skipped only when the rule `git check-ignore -v` names is outside every `# [teamai:` block. The `credentials` owner already lists every path, ignored or not; a test now covers that too. * fix(mcp): judge each CodeBuddy lookup file with the tools that write it (#993) Cleaning CodeBuddy's other user MCP files reused the ownership check built for the file it reads, which knew of no other tool writing there. With `~/.codebuddy/mcp.json` linked to Claude's `~/.claude.json`, Claude's copy of a team server matched CodeBuddy's render and was deleted through the link. Each file is now judged with the records of every tool whose MCP file it is. * fix(docs): judge a deactivated namespace's doc by its type as well as its bytes (#993) When a docs namespace stops being active here, its copies that are teamai's are removed. A member's regular file whose bytes equal the target of a team doc that is a link was taken for teamai's, through the link and through a history check that also matched link versions, and deleted. A file is teamai's only as a team file now, today's or one from the history, as on every other docs path that deletes or replaces (prune, copy, uninstall). * test(hooks): check a Codex dispatcher entry outlasts its slowest team hook (#915) Codex stops the dispatcher entry at its own timeout, so it has to cover every team hook it runs, counting Codex's 600 s default for a hook that names none. * fix(mcp): count another tool's servers in a shared file even when it is not detected (#993) Whose entries a config file holds was judged only with the tools detected on this run. With `~/.codebuddy/mcp.json` linked to Claude's `~/.claude.json` and Claude no longer installed, a CodeBuddy pull took Claude's identically rendered server for its own and deleted it through the link, leaving Claude's record stale. Every tool that maps a file counts now, detected or not: detection decides where teamai writes, never what it may delete. Doctor judges the same way. * fix(mcp): restore a file two tools write through linked paths once, to its first state (#993) Configs written during an MCP sync are restored when ownership cannot be saved. Snapshots were keyed by path, so a file two tools reach through linked paths got one per path and the later one, taken after the first tool's write, was restored last: the server stayed without its record. One snapshot per real file now, taken before its first write. * fix(coauthor): leave a settings file that does not parse as it is (#993) Project scope writes the co-author setting to `.claude/settings.local.json`, the member's own file. One that did not parse was read as empty, so a pull replaced the whole file with teamai's `attribution` block and the member's settings were lost. A Claude or Cursor file that is not a JSON object is now left untouched and reported as skipped, and the setting is written on the first pull after the member repairs it. * fix(hooks): leave a hook settings file that does not parse as it is (#993) Every hook writer read its settings file as empty when it did not parse, so a pull or a session-start pass while the member was mid-edit of, say, their HOME `~/.claude/settings.json` replaced the whole file with teamai's hooks. The Claude, Cursor, Copilot, Codex and ZCode writers, and the agent-hook install and removal, now leave such a file byte-identical and say which file and why; nothing is recorded, and the first pull after the repair writes it. * fix(docs): never walk a docs mirror that is a link of the member's (#993) When `sharing.docs.localDir` itself was a link to a personal directory, uninstall walked it and deleted the files behind it that matched a team doc from the history, outside the mirror. A mirror root that is a link is now the member's on every path: pull copies, prunes and withdraws nothing through it and names it, doctor reports it, and uninstall leaves it and what it points at. * fix(uninstall): keep the record of hooks a settings file that does not parse still holds (#993) Uninstall left out a hook settings file that did not parse, then deleted the data directory holding the record of its hooks; once the member repaired the file, teamai's hooks ran there with no record left to remove them by. Such a file is now tried, named and left as it is, and the data directory stays, so uninstall run again after the repair removes them. `source remove-http` and uninstall keep the record of an HTTP agent hook they could not remove, and the local agent's directory with it, instead of clearing the whole manifest. Pull also names a docs mirror that is a link before counting or previewing its files, so it never walks it, on a dry run either. * fix(uninstall): report an uninstall that left hooks in place as incomplete (#993) When a hook settings file could not be cleaned, uninstall kept the data directory but still said "teamai uninstalled" and exited 0, and a targeted uninstall of the last tool did not record the tool as excluded, so the hook left in place could sync its resources back. Hooks left in place now make the uninstall incomplete, as an OpenCode entry it could not remove does: exit code 1, the files named, and the tool excluded. The message for a settings file that does not parse no longer points at `teamai pull`. * fix(uninstall): stop hooks an incomplete uninstall left in place from syncing back (#993) A full uninstall that could not remove a tool's hooks kept the data directory with every tool still enabled, so the hook left in place ran a session-start pull that restored what had just been removed. Each tool whose hooks remain is now excluded, and the session-start pull of an excluded tool's hook does nothing, as its instruction and HTTP handlers already did, until the uninstall is run again after the repair. * fix(local-agent): keep removed HTTP sources disabled (#993) * fix(local-agent): serialize HTTP sync and source removal (#993) * docs(http): describe skipped concurrent hook syncs (#993) * fix(mcp): take teamai's Claude local-scope servers out of every recorded key on uninstall and flag off (#915) Uninstall and a pull with sharing.gitExclude off took teamai's servers out of the current checkout's key only. A key no checkout has any more, as a removed linked worktree of a --separate-git-dir repository leaves, kept them, with their resolved values, for good. Both now clean every key recorded in managed-local-mcp.json; a server the member changed stays and is named, except on uninstall. * feat(mcp): give CodeBuddy the team's project MCP servers in its local scope (#915) With sharing.gitExclude on, CodeBuddy's project servers go to ${CODEBUDDY_CONFIG_DIR:-~}/.codebuddy.json under projects[<realpath of the worktree root>].mcpServers, the key CodeBuddy uses for the directory it runs in, instead of the project's .mcp.json. - Each checkout writes its own key, on pull and on the pull git runs when it creates a worktree; records live with Claude's local-scope records, keyed by tool and key. - The next pull takes teamai's servers out of the key of a worktree that is no longer a checkout of the repository, keeping the member's own and a copy they changed (named). - Migration and flag-off follow Claude's: teamai's servers leave .mcp.json (recorded or proven by the team's history), the file goes when only they made it, and turning the option off moves them back. - Uninstall removes exactly teamai's servers from every checkout's key. * fix(opencode): on V2, take teamai's V1 entries out of a file that also holds the member's (#915) On OpenCode V2 with teamai's plugin, a pull left the root opencode.json and .opencode/opencode.json as they were once they held anything of the member's, so V2 loaded teamai's servers twice. It now takes teamai's own servers (recorded, or adopted by the per-name judgement) and its instructions entries (equal to the ones it writes) out of a file git does not track, and keeps the member's. A file git tracks is still never modified, and doctor still names it. * fix(uninstall): remove the HTTP source under its lock before teardown (#993) A full user-scope uninstall removed agent hooks, plugins and ~/.teamai without the local-agent lock, so a hook sync paused after loading its config could reinstall resources once uninstall reported success. Uninstall now runs the remove-http shutdown first: it waits up to 30 seconds for the lock, disables the source before teardown, and exits 1 without removing anything when the lock cannot be taken. * fix(local-agent): let a server-pushed uninstall hold its sync's lock (#993) A sync holds the HTTP source lock while it runs an uninstall_teamai command, so the uninstall it spawns waited for that lock until the hook's 15-second budget killed the sync, unacknowledged and with teamai still installed. The sync now passes its pid to the command; a child whose parent holds the lock runs under it without acquiring or releasing it. * test: stop hooks-wrapper from writing src/index.js, which shadowed the CLI entry (#915) hooks-wrapper.test.ts wrote a stub src/index.js for the length of each test so resolveTeamaiEntryScript() would find an entry. While it existed, any test file running in parallel that imported '../index.js' resolved to the stub instead of src/index.ts and got an empty module, so `program` was undefined: a different command-table test failed on each loaded run. The test now reports src/index.js as present through an existsSync spy and writes nothing into the source tree. Tests that read the command table load it through helpers/command-table.ts, which fails with the reason when the entry yields no table instead of a TypeError. * refactor(mcp): judge local-scope claims on the reconcile's claim targets (#915) Leaving a tool's other MCP location re-resolved every tool's ta…
Closes #993
Summary
--separate-git-dir, submodule)git worktree list.agents/skills.git/info/withholds project MCP.teamai/last-fetch.json, a #964 regression already onmain).claude/settings.jsonsettings.local.jsoninit . --agent claudesets up every HOME toolEvidence
Bug 1, built CLI,
git init --separate-git-dir,git worktree add(its post-checkout pull runs in the worktree), then a rule edited in the main checkout:Local-agent rules, built CLI against an HTTP mock:
hook-dispatch session-start --tool clauderunsinstall_rulefor two rules, the member replaces one copy with their own text, and a second session start runsuninstall_rulefor both:.claude/rules/(no record, and the resource cache has no history).success.Bug 5, built CLI, repo made with
git init --template=(no.git/info/):Bug 10, built CLI, user scope, the member adds a server with
codebuddy mcp add -s userafter teamai wrote~/.codebuddy/mcp.json:~/.codebuddy/.mcp.json; teamai's servers are hidden and nothing reports it.codebuddy mcp listcheck, because the CodeBuddy CLI is not installed on the machine that ran this. CI asserts the chosen file.Bug 4, built CLI, git project,
init --agent claude,copilotwith Copilot team hooks,pull,teamai packages install(stubnpm):?? .teamai/ingit status(managed-hooks.json,teamai.lock, and since fix: sync team resources before AI sessions start #964last-fetch.jsonafter a single-repo mode switch).git status --porcelain -uall --ignored -- .teamaiis empty;teamai.lockandworkspaces/<id>/managed-hooks.jsonare in the data home; the Copilot hook file holds the team hook.Bugs 2 and 12, built CLI: the member has their own
.claude/agents/team-agent.mdbefore init; later.gitis restored with a new inode, two rules are edited, and the role switches:Bug 12 (skills, docs), built CLI: the member has their own
.claude/skills/team-skill/SKILL.mdand.teamai/docs/guide.mdbefore init:Bug 9, built CLI, sandboxed HOME with five tool dirs:
init . --agent claudeenables and writes hooks for all five tools.Bug 7, built CLI, business repo tracks
.claude/settings.json, team setssharing.coAuthor.enabled: false:M .claude/settings.jsonplus a whole-file reformat.attributionlines fromsettings.jsonand puts the value insettings.local.json.Claude Code 2.1.294 merges
attributionper key across the two files. Two real commits throughclaude -p --setting-sources project,local, in throwaway repos withsettings.json={commit: "Co-Authored-By: SHARED-COMMIT <shared@example.com>", pr: "SHARED-PR"}:settings.local.json={pr: "LOCAL-PR"}: the commit carries the SHARED-COMMIT trailer, so the local object does not replace the shared one.settings.local.json={commit: "", pr: ""}: the commit has no trailer, so the local key wins over the shared key.Bug 6, built CLI, OpenCode V2 project with team rules:
instructionsentries; no team rules reach the session; doctor reports OpenCode ready.instructions.Bug 9, fresh clone of a
mode: selfrepo (committed marker, no local config):init . --agent claudethen stopped at "already initialized".initloads config withselfHeal: false;--agent claudeenables only Claude (with and without--force). With no--agentand no TTY, init does not prompt and enables the HOME-detected tools, as for a fresh repo. Other commands still self-heal (pullon the clone enables all five).HTTP source removal after malformed hook settings, built CLI with a local HTTP fixture:
1e82f350):source remove-httpkeeps the active endpoint and returns exit code 0. The new real-CLI regression fails on that head.256679d3:npm run build,npx tsc --noEmit,npm run lint,npx vitest run(8234 passed, 19 skipped); real-CLI validation:npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/local-agent-rules-993.test.ts src/__tests__/e2e/mcp-uninstall.test.ts src/__tests__/e2e/managed-resources-uninstall.test.ts --retry=0(4 passed, no retries).HTTP shutdown while another process is waiting for an HTTP response, built CLI:
256679d3):source remove-httpfinishes before the paused Stop hook, which reinstalls a rule after removal. A paused plugin reconcile worker similarly recreates its state after cleanup. Both new barrier tests fail on that head.dbdfa1d9:npm run build,npx tsc --noEmit,npm run lint,npx vitest run(8235 passed, 19 skipped);npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/local-agent-rules-993.test.ts src/__tests__/e2e/mcp-uninstall.test.ts src/__tests__/e2e/managed-resources-uninstall.test.ts --retry=0(6 passed, no retries).Full uninstall while a hook sync is waiting for an HTTP response, built CLI:
9a763382):teamai uninstall --forcedeletes~/.teamaiwhile the Stop hook is paused; the hook then reinstalls a rule and recreates local-agent state. The new barrier test fails on that head for an HTTP-only install and for a user-scope git team install with an HTTP source.source remove-httpshutdown first, under the same lock: it waits for the paused sync, disables the source, removes what the sync installed, then continues. Both cases pass: exit code 0, no rule left, no~/.teamai.2a6e1b67:npm run build,npx tsc --noEmit,npm run lint,npx vitest run(8235 passed, 19 skipped);npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/local-agent-rules-993.test.ts(6 passed).Server-pushed uninstall of the last user-scope tool, built CLI:
2a6e1b67): the Stop hook's sync holds the lock while it runsuninstall_teamai(teamai uninstall --force --agent workbuddy); the uninstall waits for that lock until the hook's 15-second budget ends the sync. No ack is sent and~/.teamaistays. The new test fails on that head.successwithin seconds and~/.teamaiis gone.d4a7e416:npm run build,npx tsc --noEmit,npm run lint,npx vitest run(8235 passed, 19 skipped);npx vitest run --config vitest.e2e.config.ts src/__tests__/e2e/local-agent-rules-993.test.ts(7 passed).Deviations
.hermes/skillsor.openclaw/skillsis kept, named on every pull and stays on record, soteamai pushdoes not offer it as a new team skill. Destinations outside the project are recorded as absolute paths inside a tool's home (the Hermes home, the OpenClaw workspace and state dir, Copilot's home, wherever that tool's team skills go); a HOME copy shared by two projects is deleted only when the last one releases it.teamai removeanduninstalldelete a skill directory in any tool's skills directory only when the checkout record or the history proof shows it is teamai's (removereads the record too, not history alone); each one kept is named.uninstallstill deletes the names the local agent's manifest lists without the history check, so HTTP-mode installs uninstall as before. Pull's cleanup of nested copies of an excluded skill keeps a member's copy.teamai removerefreshes rules with the checkout record; three.cursor/rules/*.mddeletions (pull's sweep, pull's per-pull older-copy cleanup, uninstall) now delete only a recorded file, a team version from history, or a built-in rule's name. The agent proof also accepts what 0.26.0 rendered (pre-aliasmodel, pre-Proposal: model aliases so one agent works in every tool #830 extras), checked by delivering with 0.26.0 and pulling with this build. A pull that keeps a member's file now always makes the next pull a full sync. An agent held for an unresolvable model is delivered by the next pull once the model resolves, also when the holding pull found the checkout at the team revision (pinned by a test, merge 06b3106; no code change needed).listWorktreesreads-zoutput; on git < 2.36 it falls back to the old newline parsing for its other callers (MCP exclusion, uninstall, hooks, dashboard), so the feat(mcp): keep project MCP configs with resolved tokens out of git (#882) #886 proven-clean check still sees sibling worktrees. Liveness never uses that fallback:completeWorktreeListreturns null there and pruning relies on recorded roots..mcp.json) stays that tool's and is skipped silently, as onmain. Without this third owner, CodeBuddy would adopt Claude's server anduninstall --agent codebuddywould delete it ([feat] keep project MCP configs with resolved tokens out of git #882 ledger).~/.codebuddy/mcp.jsonthat holds only teamai's servers is emptied into the next file in CodeBuddy's lookup order and deleted; a file with any member server or other key is left and stays chosen. Pull explains the move once.lastPullRev(as for held agents), so every pull is a full sync until the member renames or deletes the file. If that proves slow, the follow-up is to advance the rev and keep the held paths in state.pull-keeps-edits-822's "no record means overwrite" case now asserts the new rule.teamai uninstall,teamai remove agentsand the namespaced copy beside an author's root copy now delete a rule or agent only when the checkout record or the team history proves it teamai's, and name each kept file. A tracked.teamai/teamai.lockis left in place and named bydoctorwithgit rm --cached. The docs prune keeps a file when the team history cannot be read. A member's file at a source skill's path is named against the source's versions. The e2e CI jobs no longer prepare a real$HOME(the e2e setup file sandboxes HOME for every test file).judgeRemovalsorts a copy of a resource no longer delivered into remove, edited (changed since teamai delivered it, or on another checkout's record, as after a restore) and not teamai's. Pull's tombstone cleanup and the rules sweep now name a member's own file asKept <path>: it is not teamai's (...), so pull left it.instead of saying the member changed a teamai copy.removeanduninstallalike. Pull and doctor name it:Kept <path>: it is a link of yours, so teamai does not replace it. Remove the link to receive <name> from the team.Behaviour change: team and source skills no longer replace a member's leaf link in place of a skill directory, and the Codex duplicate cleanup no longer deletes a configured leaf link (a24a9b9, 9a0edf0, 99d7102, merge 92b5ee6).pushskip links in a skill's source and name each one (merge 92b5ee6, e5b4479). A docs-mirror link at a path the team never had is kept and named instead of unlinked (behaviour change).uninstallremoves from the docs mirror only what the team history proves teamai's, keeps and names the rest, and removes directories once empty (it used to delete the whole mirror).uninstall(merge 970a43b).opencode.json,openclaw.json): entries are written at the link's target, so the link survives; a resolved value written through a link is kept out of git in the target's repository, or withheld when the target is tracked there (merge 756d8e9).CLAUDE.md/AGENTS.mdis never deleted when teamai's section goes (1c21e8f). A tombstoned agent in a team-defined tool is removed only on proof (62b6019)..gitnever reads an enclosing repository's history (9be673d).uninstallkeeps the member's hidden files in the docs mirror; a docs link is teamai's only when the team had a link there, not a file with the same text (1761e77)..mcp.jsonlinked tomcp.json(or~/.codebuddy.jsonlinked back) is one file and nothing is moved or deleted through the alias; with the manifest lost, teamai's copies left in a lookup file CodeBuddy does not read are removed on pull anduninstall, and the member's stay (merge 5c5dd0c). A source skill's link path keeps a member's regular file there (1107d46).CONTRIBUTORSfile needs the same proof as any file, and a link in the team history never proves a regular file (3e9432f).removeanduninstalldelete only the files on record or matching a team version, name each file they keep, and leave the directory while anything is in it. A team removal on pull keeps a directory holding a file of the member's whole and names it; source skill removal does the same (988e442, fb2982d).git cat-file --batch(about 30 ms for 60 versions, was about 370 ms with one git call per version) and only when a target file holds an unrecorded entry; a co-author record stays while its shared settings file does not parse, so a pull after the member repairs it removes the old value.uninstallwhen it equals a render of that server from the team history, in.mcp.json-style files and Codex config alike. A member's entry under that name stays..mcp.json-style files and Codex alike;--dry-runsaysWould record MCP server <name> in <file> as teamai'sand writes nothing. Real-CLI tests: adoption then team removal, adoption thenteamai uninstall. Hook adoption already saved its records (no change).<stem>.tomlincluded. It now deletes a sibling only when the checkout record or the team history proves it teamai's, and names one it keeps. Built-in agents keep sweeping their own names (no team history, as for built-in rules).SKILL.mdincluded) is kept and named byteamai source removeand by a pull after the source withdraws the skill; a file where a team skill directory goes, or a directory where a rule or agent file goes, is the member's: kept and named, and the first pull after it is gone delivers the team version;teamai remove skillson a name the CLI once shipped (such asteam-wiki-codebase) removes the team's and the packaged files and keeps a file the member added, asuninstalldoes.~/.codebuddy/.mcp.jsonlinked tomcp.jsonan older record without a file is kept and the server reinstalls.uninstallkeeps OpenCode's user plugin, which now also carries V2's rules and instructions to every project and the user scope, and names it, as for the other global adapters (a user-scope uninstall andteamai hooks removestill remove it); a skill directory holding a.git(a repository the member made there) is never deleted whole byremove,uninstallor a source removal; moving a CodeBuddy server to the file CodeBuddy now reads keeps the old copy when the member edited it.uninstall --agent opencoderecords the exclusion even when the only teamai plugin left is a server-pushed agent hook, and names the kept plugins; a member's regular doc with the bytes of a team doc that is a link is the member's, not replaced by the link; CodeBuddy's other user MCP files are each judged with the tools that write them, so a lookup file linked to Claude's~/.claude.jsonnever loses Claude's server (ebe740e).~/.claude.jsonnever loses Claude's server after Claude is uninstalled (detection decides where teamai writes, never what it may delete; doctor judges the same way); a sync that cannot save ownership restores each config file once, by real path, to its state before the run..claude/settings.local.jsonmid-edit) is left untouched and reported as skipped, instead of being replaced by teamai'sattributionblock; the next pull after it is repaired writes the setting.sharing.docs.localDir) that is itself a link is the member's: pull delivers nothing through it, doctor reports it, and uninstall leaves it and what it points at.teamai uninstalltries a hook settings file that does not parse, names it, and keeps the data directory holding the record of its hooks, so uninstall run again after the repair removes them, and such an uninstall reports itself incomplete (exit code 1) and excludes the tool it targeted and every tool whose hooks remain, whose session-start pull then does nothing;teamai source remove-httpand uninstall keep the record of an HTTP agent hook they could not remove, and the local agent's directory with it; pull names a docs mirror that is a link before counting or previewing its files, so it is never walked, on a dry run either.256679d3): the local agent retains an endpointless{disabled:true}config after removal, preventing legacy config or environment fallback from reactivating the source. Failed hook records survive separately and report exit code 1. Hook cleanup works without an active config; HTTP initialization replaces the disabled config.dbdfa1d9): active hook syncs and detached plugin reconciliation finish under the same lock that guards removal. Hook sync skips on contention; plugin reconciliation and removal wait up to 30 seconds. Removal does no teardown and reports exit code 1 if it cannot acquire the lock. The lock lives outside source cache cleanup, and each operation reloads config after acquisition.2a6e1b67): a user-scopeteamai uninstallremoves the HTTP source the waysource remove-httpdoes before its other steps, so it waits up to 30 seconds for the lock and exits 1 without removing anything when it cannot take it. An uninstall with no team config (an HTTP-only machine) now also uninstalls the source's resources and agent hooks, which it used to leave behind when it deleted~/.teamai; a hook it cannot remove keeps~/.teamaifor a retry.d4a7e416): a sync runsuninstall_teamaiwhile holding the lock and passes its pid inTEAMAI_LOCAL_AGENT_LOCK_HOLDER; the HTTP shutdown in a child whose parent holds the lock (by that pid, checked against the lock file) runs under it, without acquiring or releasing it.mcp-git-info-993now waits for the detached session-start background pass before deleting its sandbox; the OpenCode V1 hooks e2e pre-installs@opencode-ai/pluginin its config dir, because OpenCode 1.18 waits at the firstPOST /sessionfor an npm install of it (reproduces onmainwith a blocked registry). The session is still created once and all assertions are kept..teamai/managed-hooks.json(rewriting it), but a tracked file is never deleted;doctornames it withgit rm --cached.doctorreads an old.teamai/teamai.lockin place without moving it.uninstallmigrates before its prompt, never on--dry-run.rootfile is not always legacy (contribute, recall, viz, the MCP writers and the local agent create it before a checkout's first full pull). It is pruned only whengit worktree list -zsucceeded and names every checkout (no--separate-git-dir/submodule main entry, git >= 2.36), and its id matches no listed checkout under either the printed path or its realpath. A directory holdinglocal-agent/is always kept. Records withoutrootare always kept, as the spec says.codex-trustandhooks-reconcile-scope. Reviewer: please confirm.pull --forceno longer replaces a member's MCP server;teamai mcp inject --forcedoes.initonly, at both config loads on its path (preAction and the pre-marker read). Every other command still self-heals..claude/settings.json(exactly teamai's value, recorded) is moved tosettings.local.jsonon the next pull, also when no co-author choice exists; anattributionthe member already has insettings.local.jsonis kept. Other Claude-family tools have no personal settings file, so with no choice their pre-fix value stays until a choice exists.Ownership coverage
Proof: R delivery record (ledger, MCP/hook manifest, source manifest), H team (or source) repo history, T today's render. Every cell refuses links at delivered paths unless noted.
teamai removeteamai uninstallmain)main; see Known gaps)uninstall_rule: T (today's render of the cached rule, read before it goes); a member's file at that path is kept.git)teamai source remove: sameuninstall(as onmain).mcp.jsonstyle, OpenCode, Copilot keyed)teamai mcp remove: R, H)main)teamai hooks remove: R, Hmain)Linked config files (MCP, hook settings, co-author,
opencode.json,openclaw.json) are the member's files that teamai edits entries in: entries are written at the link's target and the link survives. Tools whose MCP paths reach the same file through a link are one file to reconciliation: a server another tool's record claims there is left to that tool.Known gaps
init --agent hermes,openclawin project scope still creates empty<project>/.hermesand<project>/.openclaw(createProjectToolRoots, predates [bug] Delivery and local-state bugs on main found while scoping #915 #993); nothing is written under them, and git does not see empty directories.Upgrading from 0.26.0: the first pull is "Already synced" (0.26.0 recorded the revision). Hooks, MCP and co-author still apply on that pull. The full-sync ownership judgement of skills, docs, rules and agents waits for the next team change or
pull --force; until then those copies are left as they are, never overwritten. [feat] keep the resources pull delivers in project scope out of git #915 forces one full sync on upgrade (with the flag on or off), pinned by a real-CLI upgrade test from 0.26.0 in its ticket 02, which closes this.Lock errors (predates this PR): only the exclude-file and
managed-mcp-files.jsonwriters (updateFileLocked) report an uncreatable lock directory as not writable; the otheracquireLockcallers still report it as busy. Tracked in [bug] A lock teamai cannot create is reported as held by another process #999.Single-repo mode: the ownership proof reads the member's business repo; if it is a shallow clone, an older unedited team copy is kept and named as the member's instead of being updated (the safe direction).
Bug 10, HTTP-mode teams: the local agent does not apply the upgrade rule (no pull runs the MCP sync for them), and a server it installed in a file CodeBuddy no longer reads moves only on reinstall. Doctor does not flag the upgrade state before the pull. Closed by [feat] keep the resources pull delivers in project scope out of git #915 ticket 19 (local-agent MCP in the tools' local scope).
Local-agent installs (HTTP-mode teams, as on
main) still overwrite a member's regular file, anduninstallremoves a skill the local agent installed by its name (a link is now kept and named): their resource cache has no git history, so the proof cannot apply; it needs the local-agent manifest as the record: [feat] keep the resources pull delivers in project scope out of git #915 ticket 07 (local-agent manifest as the record for installs;teamai removeof skills and rules already proves a copy by today's render).HTTP resource cleanup still defaults to WorkBuddy because the resource manifest does not record its owning tool.
source remove-httpcan leave resources installed for another tool even without concurrency; per-tool ownership belongs with [feat] keep the resources pull delivers in project scope out of git #915's local-agent manifest work.Merge Danger
Door: two-way
Blast Radius: delivery
Pull, init and delivery for every tool change. A wrong ownership decision either keeps a stale team file or overwrites a member file; regression tests cover both directions per resource kind.