Skip to content

fix: delivery and local-state bugs (#993) - #995

Merged
jeff-r2026 merged 196 commits into
Tencent:mainfrom
SaulMoro:fix/993-delivery-bugs
Oct 9, 2026
Merged

jeff-r2026 merged 196 commits into
Tencent:mainfrom
SaulMoro:fix/993-delivery-bugs

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #993

Summary

# Bug Fix In
1 Linked-worktree pull deletes the main checkout's state (--separate-git-dir, submodule) probe each recorded checkout instead of trusting git worktree list yes
2, 12 No delivery record: pull overwrites member rules and agents ownership proof without a record yes
12 Same for skills and docs same proof, per file yes
12 Same for MCP servers and hook entries in shared config files per-entry ownership yes
3 Codex overwrites the member's skill in .agents/skills never overwrite a foreign skill yes
8 Source skills go to dirs the tool never reads, and to tools not enabled source skills follow team-skill delivery yes
5 Missing .git/info/ withholds project MCP create it yes
4 Machine state written into the working tree (also .teamai/last-fetch.json, a #964 regression already on main) move it to the data home yes
6 OpenCode V2 gets no team rules or instructions inject them through the plugin yes
7 Project-scope co-author setting written into tracked .claude/settings.json write it to settings.local.json yes
9 init . --agent claude sets up every HOME tool union with the real pre-init config only yes
10 CodeBuddy hides teamai's or the member's user MCP servers write to the file CodeBuddy reads yes
 teamai init . --agent claude          # single-repo
-  enabled = every tool found in HOME
+  enabled = --agent tools ∪ tools enabled before this init
 project-scope co-author setting
-  .claude/settings.json        (tracked, team file)
+  .claude/settings.local.json  (untracked, member file)

Evidence

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:

  • Before: the worktree's pull pruned the main checkout's record and workspace directory; the next pull in main overwrote the edited rule.
  • After:
    workspaces/: 5a4e17c868d7 e049c638313b      # both checkouts kept
    $ teamai pull --force
    ℹ [project] Kept .../sep/.claude/rules/team-rule.md: you changed it since teamai delivered it. ...
    

Local-agent rules, built CLI against an HTTP mock: hook-dispatch session-start --tool claude runs install_rule for two rules, the member replaces one copy with their own text, and a second session start runs uninstall_rule for both:

  • Before: the installed copy stays in .claude/rules/ (no record, and the resource cache has no history).
  • After: the installed copy is gone and the member's file is kept; both commands acked success.

Bug 5, built CLI, repo made with git init --template= (no .git/info/):

  • Before: the MCP server with a resolved value is withheld, with a warning that blames permissions.
  • After:
    $ teamai init <url> --provider git --agent claude --scope project
    ✔ teamai initialized successfully!
    $ cat .git/info/exclude
    # [teamai:mcp-exclude:start] project MCP configs holding resolved ${VAR} values
    /.mcp.json
    # [teamai:mcp-exclude:end]
    $ git status --porcelain      # ?? .claude/   (no .mcp.json)
    $ teamai doctor               # ✔ MCP servers delivered to claude
    

Bug 10, built CLI, user scope, the member adds a server with codebuddy mcp add -s user after teamai wrote ~/.codebuddy/mcp.json:

  • Before: CodeBuddy reads only the new ~/.codebuddy/.mcp.json; teamai's servers are hidden and nothing reports it.
  • After:
    $ teamai doctor
      ✖ codebuddy reads the file holding teamai's MCP servers
        → teamai's MCP servers for codebuddy (tm-user) are in .../.codebuddy/mcp.json, which codebuddy does not read ... Run `teamai pull` to move them there.
    $ teamai pull --force
    ℹ Took teamai's MCP servers for codebuddy (tm-user) out of .../.codebuddy/mcp.json: codebuddy reads only .../.codebuddy/.mcp.json, ...
    ~/.codebuddy/.mcp.json: {"mcpServers":{"my-user":{...},"tm-user":{...}}}
    ~/.codebuddy/mcp.json:  {"mcpServers":{"mine-old":{...}}}
    
    Not run: the manual codebuddy mcp list check, 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,copilot with Copilot team hooks, pull, teamai packages install (stub npm):

  • Before: ?? .teamai/ in git status (managed-hooks.json, teamai.lock, and since fix: sync team resources before AI sessions start #964 last-fetch.json after a single-repo mode switch).
  • After: git status --porcelain -uall --ignored -- .teamai is empty; teamai.lock and workspaces/<id>/managed-hooks.json are 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.md before init; later .git is restored with a new inode, two rules are edited, and the role switches:

  • Before: init overwrote the member's agent; after the restore, pull overwrote the edited rule and deleted the edited rule of the old role.
  • After:
    $ teamai init ... --role frontend
    ⚠ [project] Kept .../.claude/agents/team-agent.md: it is not teamai's (no delivery record, and it matches no team version of agents/team-agent.yaml). Rename or delete it, then run teamai pull, to receive the team version.
    $ (restore .git with a new inode; edit two rules) teamai roles set backend; teamai pull
    ⚠ Kept .../.claude/rules/frontend/fe-rule.md: teamai no longer delivers frontend/fe-rule here, but you changed this copy. Delete it when you no longer need it.
    ℹ [project] Kept .../.claude/rules/team-rule.md: you changed it since teamai delivered it. ...
    

Bug 12 (skills, docs), built CLI: the member has their own .claude/skills/team-skill/SKILL.md and .teamai/docs/guide.md before init:

  • Before: init overwrote both.
  • After:
    $ teamai init ...
    ⚠ [project] Kept .../.claude/skills/team-skill: it is not teamai's (no delivery record, and it matches no team version of skills/team-skill). Rename or delete it, then run teamai pull, to receive the team version.
    ⚠ [project] Kept .../.teamai/docs/guide.md: it is not teamai's (no delivery record, and it matches no team version of docs/guide.md). ...
    $ teamai doctor
      ✖ Skills delivered to claude
      ✖ Team docs delivered
    

Bug 9, built CLI, sandboxed HOME with five tool dirs:

  • Before: init . --agent claude enables and writes hooks for all five tools.
  • After:
    $ teamai init . --provider git --agent claude
    ✔ teamai initialized (single-repo mode)!
    $ git status --porcelain -uall
    $ grep -A2 enabledAgents ~/.teamai/projects/*/config.yaml
    enabledAgents:
      - claude
    

Bug 7, built CLI, business repo tracks .claude/settings.json, team sets sharing.coAuthor.enabled: false:

  • Before: M .claude/settings.json plus a whole-file reformat.
  • After:
    $ teamai init <team-url> --provider git --agent claude --scope project
    $ git status --porcelain
    ?? .claude/settings.local.json
    ?? .claude/skills/
    $ git diff
    
    Upgrade path: a pull after 0.22.0 removes only the five attribution lines from settings.json and puts the value in settings.local.json.
    Claude Code 2.1.294 merges attribution per key across the two files. Two real commits through claude -p --setting-sources project,local, in throwaway repos with settings.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:

  • Before: V2 ignores the instructions entries; no team rules reach the session; doctor reports OpenCode ready.
  • After: the teamai plugin adds the rules and instructions through the V2 context hooks; doctor fails once the plugin is removed; V1 still reads instructions.

Bug 9, fresh clone of a mode: self repo (committed marker, no local config):

  • Before: the self-heal ran before init and enabled every HOME tool; init . --agent claude then stopped at "already initialized".
  • After: init loads config with selfHeal: false; --agent claude enables only Claude (with and without --force). With no --agent and no TTY, init does not prompt and enables the HOME-detected tools, as for a fresh repo. Other commands still self-heal (pull on the clone enables all five).

HTTP source removal after malformed hook settings, built CLI with a local HTTP fixture:

  • Before (1e82f350): source remove-http keeps the active endpoint and returns exit code 0. The new real-CLI regression fails on that head.
  • After: removal returns exit code 1, leaves the settings bytes and failed hook record intact, and disables the source. The fixture confirms requests before removal and none from subsequent dispatched hooks, even with legacy HTTP config and an endpoint environment variable. Repairing the settings and retrying returns 0, removes the hook, preserves personal settings, and keeps the source disabled.
  • Three unit cases cover no fallback, legacy config, and environment fallback, through failed removal, repair/retry, and explicit HTTP reinitialization.
  • Validation at 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:

  • Before (256679d3): source remove-http finishes 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.
  • After: sync, plugin reconciliation and removal share one lifecycle lock. Removal waits for the active operation, reloads config, disables the source before teardown, then removes its resources and caches. Both tests pass. A held lock that cannot be acquired produces exit code 1 without modifying the source.
  • Validation at 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:

  • Before (9a763382): teamai uninstall --force deletes ~/.teamai while 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.
  • After: uninstall runs the source remove-http shutdown 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.
  • Validation at 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:

  • Before (2a6e1b67): the Stop hook's sync holds the lock while it runs uninstall_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 ~/.teamai stays. The new test fails on that head.
  • After: the sync passes its pid to the command; the uninstall, its child, runs under the lock it holds. The command is acked success within seconds and ~/.teamai is gone.
  • Validation at 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

  • Bug 8: the source manifest is the delivery record. An unrecorded copy is the source's only if it equals today's source skill or a version in the source repo's history; otherwise it is the member's (kept, named), at every source destination, not only Codex's shared directory. A changed legacy copy in the project's .hermes/skills or .openclaw/skills is kept, named on every pull and stays on record, so teamai push does 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.
  • Bug 3 (follow-up): teamai remove and uninstall delete a skill directory in any tool's skills directory only when the checkout record or the history proof shows it is teamai's (remove reads the record too, not history alone); each one kept is named. uninstall still 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.
  • Bugs 2 and 12 (follow-ups): teamai remove refreshes rules with the checkout record; three .cursor/rules/*.md deletions (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-alias model, 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).
  • Bug 1: listWorktrees reads -z output; 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: completeWorktreeList returns null there and pruning relies on recorded roots.
  • Bug 12 (skills, docs): one member file in an unrecorded skill directory makes the whole directory the member's. The docs mirror keeps no record, so an edited copy of a delivered doc is now kept (it used to be overwritten). When the team deletes a doc, a member's file at that path is kept and named; a file of the member's at a path the team never had stays without a word (behaviour change: the mirror used to prune it, 5eb72ed).
  • Bug 12 (MCP): an entry the target tool's record does not claim but another tool's record claims in the same file (Claude and CodeBuddy share .mcp.json) stays that tool's and is skipped silently, as on main. Without this third owner, CodeBuddy would adopt Claude's server and uninstall --agent codebuddy would delete it ([feat] keep project MCP configs with resolved tokens out of git #882 ledger).
  • Bug 10: the local agent (HTTP mode) uses the same CodeBuddy file choice. An older teamai's ~/.codebuddy/mcp.json that 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.
  • Bugs 2 and 12 (rules, agents): a restored copy and a member's own file both lack a record; the kept-edit wording is chosen when another checkout record in the same state lists that exact path (the record lost to a new key). That record only picks the message; nothing is carried over.
  • Bugs 2 and 12: a pull that keeps a member's file does not advance 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.
  • Bugs 2 and 12: a personal rule whose name the team never had is now kept, silently; the stale sweep used to delete it. This changes what members saw before; the usage guide says so.
  • Bugs 2 and 12: the agent proof checks every team agent with the same name stem in any namespace, since agents share one flat directory.
  • Bugs 2 and 12: pull-keeps-edits-822's "no record means overwrite" case now asserts the new rule.
  • Final review fixes (merge 23a0af6): teamai uninstall, teamai remove agents and 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.lock is left in place and named by doctor with git 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).
  • Kept copies of removed resources (a3ba12d): judgeRemoval sorts 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 as Kept <path>: it is not teamai's (...), so pull left it. instead of saying the member changed a teamai copy.
  • CI Codex review P1 (202a53e): the docs 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. Such an entry is now 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): it is kept and named, and nothing is written over it. A link at a doc's path or at one of its ancestors is 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 (9309e20).
  • Review round on links and every removal path:
    • A link at a delivered path (skill directory, rule or agent file, docs entry, source skill destination), or inside a delivered skill directory, is the member's: never adopted, written through, replaced 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. 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).
    • teamai never creates a link: delivery and push skip 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).
    • uninstall removes 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).
    • Hooks: ownership recovery covers Claude, Codex, Cursor, Copilot and ZCode, on pull and on uninstall (merge 970a43b).
    • Linked config files (MCP, 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).
    • A linked CLAUDE.md/AGENTS.md is never deleted when teamai's section goes (1c21e8f). A tombstoned agent in a team-defined tool is removed only on proof (62b6019).
    • Bug 8: a record into a removed custom OpenClaw workspace stays valid, and a source cache without .git never reads an enclosing repository's history (9be673d).
    • A record into an OpenClaw workspace the member has since reconfigured is dropped and the old copy left as it is (6bab1ed). uninstall keeps 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).
    • CodeBuddy user MCP: lookup files are compared by real path, so a .mcp.json linked to mcp.json (or ~/.codebuddy.json linked 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 and uninstall, and the member's stay (merge 5c5dd0c). A source skill's link path keeps a member's regular file there (1107d46).
    • A file the member added to a recorded team or source skill is kept, with the copy, when the team or the source later delivers a file at that path (606a803). A skill's CONTRIBUTORS file needs the same proof as any file, and a link in the team history never proves a regular file (3e9432f).
    • Ownership of a skill directory is per file: remove and uninstall delete 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).
  • CI Codex review P1 follow-ups (b879988, cebb3c4, 3d8003e): the docs prune keeps a member's link where the team deleted a doc (a link is removed only when it is a version of that doc from the team history; a link is never followed); the team's MCP history is read once per pull with one 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.
  • CI Codex review P1s (0b7d241, 3a4cf2f): when the team deletes a doc file the member had replaced with a directory, the prune kept that directory whole instead of deleting its files; and an unrecorded copy of a server the team deleted (an older release installed it, the manifest was lost) is now removed by pull and uninstall when 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.
  • CI Codex review P1, MCP adoption (b62e503): an unrecorded server equal to today's team render was adopted but, with nothing to rewrite, the manifest was not saved; a later team removal or uninstall then left it installed. The reconcile now saves the manifest whenever the run changed it, for .mcp.json-style files and Codex alike; --dry-run says Would record MCP server <name> in <file> as teamai's and writes nothing. Real-CLI tests: adoption then team removal, adoption then teamai uninstall. Hook adoption already saved its records (no change).
  • Agent siblings (fd42d0b): re-rendering an agent in a new format deleted every same-stem file with another extension, a member's own <stem>.toml included. 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).
  • Review fixes (ce965cc, 9b54a57, 16f76bc, 46fb889): a server another tool's record claims is told by the file's real path, so a tool whose MCP path links to another tool's file no longer adopts that tool's server; a source skill copy whose every file the member edited (a single SKILL.md included) is kept and named by teamai source remove and 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 skills on a name the CLI once shipped (such as team-wiki-codebase) removes the team's and the packaged files and keeps a file the member added, as uninstall does.
  • Review fixes (eefb0e2, f6df181, 04d1243): a 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, the rest of the skill is delivered, and the next pull is a full sync that delivers them once it is gone (a source skill copy is kept whole, as for a file of the member's at a new source path); a skill, rule or agent copy that fails with an error no longer counts the pull as synced, so the next pull retries it; the local agent matches CodeBuddy's user MCP records to the install target by real path, so with ~/.codebuddy/.mcp.json linked to mcp.json an older record without a file is kept and the server reinstalls.
  • More review fixes (89385e8, 02309b4, 5797912): a project-scope uninstall keeps 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 and teamai hooks remove still remove it); a skill directory holding a .git (a repository the member made there) is never deleted whole by remove, uninstall or a source removal; moving a CodeBuddy server to the file CodeBuddy now reads keeps the old copy when the member edited it.
  • Review fixes (22bbfa3, 4d80ff4, 2cf73ca): moving a CodeBuddy server to the file CodeBuddy now reads keeps the old file's record until the new file is written, so a failed write leaves the server where uninstall finds it; a project-scope uninstall --agent opencode records 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.json never loses Claude's server (ebe740e).
  • Review fixes (915f3ce, 7a451ff, b461dcc): a deactivated docs namespace's doc is judged by its type as well as its bytes, so a member's regular file with the bytes of a team doc that is a link is kept, as on every other docs path that deletes or replaces (prune, copy, uninstall); whose servers a shared MCP file holds is judged with every tool that maps it, detected on this machine or not, so a CodeBuddy lookup file linked to ~/.claude.json never 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.
  • Review fix (co-author, eec032b): a Claude or Cursor settings file that does not parse (such as a member's .claude/settings.local.json mid-edit) is left untouched and reported as skipped, instead of being replaced by teamai's attribution block; the next pull after it is repaired writes the setting.
  • Review fixes (687ddd3, 05e7a85): every hook writer (Claude, Cursor, Copilot, Codex, ZCode, and agent-hook install and removal) leaves a settings file that does not parse byte-identical and names it, instead of replacing it whole, and the first pull after the repair writes it; a docs mirror (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.
  • Review fixes (7cb84ef, 5fe6f91, 1e82f35): teamai uninstall tries 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-http and 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.
  • HTTP removal follow-up (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.
  • HTTP shutdown concurrency (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.
  • Uninstall and the HTTP source (2a6e1b67): a user-scope teamai uninstall removes the HTTP source the way source remove-http does 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 ~/.teamai for a retry.
  • Server-pushed uninstall (d4a7e416): a sync runs uninstall_teamai while holding the lock and passes its pid in TEAMAI_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.
  • CI flakes fixed in tests, not product code: mcp-git-info-993 now waits for the detached session-start background pass before deleting its sandbox; the OpenCode V1 hooks e2e pre-installs @opencode-ai/plugin in its config dir, because OpenCode 1.18 waits at the first POST /session for an npm install of it (reproduces on main with a blocked registry). The session is still created once and all assertions are kept.
  • Bug 4: records move out of a tracked legacy .teamai/managed-hooks.json (rewriting it), but a tracked file is never deleted; doctor names it with git rm --cached. doctor reads an old .teamai/teamai.lock in place without moving it. uninstall migrates before its prompt, never on --dry-run.
  • Bug 5: when the exclude file exists, both it and its directory must be writable (the file is replaced through a temp file and a rename).
  • Bug 1: a workspace directory without a root file 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 when git worktree list -z succeeded 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 holding local-agent/ is always kept. Records without root are always kept, as the spec says.
  • Bug 12 (config entries): Cursor, Copilot and ZCode hook files still decide ownership by command alone (the ticket covered Claude and Codex).
  • Bug 12 (config entries): an entry identical to teamai's render is teamai's, even if the member wrote it; this reverses earlier assertions in codex-trust and hooks-reconcile-scope. Reviewer: please confirm. pull --force no longer replaces a member's MCP server; teamai mcp inject --force does.
  • Bug 9: the self-heal is skipped for init only, at both config loads on its path (preAction and the pre-marker read). Every other command still self-heals.
  • Bug 7: a value an older teamai wrote into Claude's shared .claude/settings.json (exactly teamai's value, recorded) is moved to settings.local.json on the next pull, also when no co-author choice exists; an attribution the member already has in settings.local.json is 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.

Resource pull adopts pull removes teamai remove teamai uninstall
Skills R, T, H R, H per file (tombstone, excluded nested); T for inactive-namespace and Codex-duplicate copies (byte-equal to today's source) R, H per file; built-in names R, H per file; built-in skills file by file; local-agent manifest names (as on main)
Skill directory, partly the member's (a file they added beside teamai's) kept whole, named kept whole, named teamai's files only; the member's and the directory stay teamai's files only; the member's and the directory stay
Rules R, T, H R, H R, H, T (today's render of the team rule, read before it goes); bare name by placement record R, H; built-in and local-agent names
Rules the local agent installs (HTTP-mode teams) install writes the copy (as on main; see Known gaps) n/a uninstall_rule: T (today's render of the cached rule, read before it goes); a member's file at that path is kept R, H; local-agent names
Agents R, T, H R, H (verbatim H for team-defined tools) R, H; built-in names R, H; built-in names
Docs mirror T, H (no record exists) H only: a file or link at a path the team never had is the member's and stays no command H per entry; the rest kept and named
Source skills R (source manifest), T, H (source repo; T only without .git) R (physical pins), plus T/H per file by bytes: once a file or the source history proves the copy the source's, a directory holding a file that matches neither (an edit, or a path the skill never had) is kept and stays on record teamai source remove: same not removed by uninstall (as on main)
MCP JSON (.mcp.json style, OpenCode, Copilot keyed) R, T, H (any revision) R, H (also servers the team removed) n/a (teamai mcp remove: R, H) R, H
MCP Codex TOML R, T, H R, H n/a R, H
MCP Copilot bare entries (pre-#882 layout) R R n/a R (as on main)
Hooks: Claude, Codex, Cursor, Copilot, ZCode R, H (exactly one team hook); Claude user scope by teamai's marker R, adopted teamai hooks remove: R, H R, H
Co-author n/a (write-only by design) pre-fix shared value: R and exact value n/a value left (as on main)

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,openclaw in project scope still creates empty <project>/.hermes and <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.json writers (updateFileLocked) report an uncreatable lock directory as not writable; the other acquireLock callers 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, and uninstall removes 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 remove of 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-http can 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.

… 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".
…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.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Disable remaining hooks after an incomplete full uninstall — src/uninstall.ts:1823: exclusions are persisted only when --agent supplies agentKey. If a normal teamai uninstall cannot remove a malformed Claude settings hook, it keeps ~/.teamai and its valid configuration but leaves every tool enabled. The surviving session-start hook then runs teamai pull and restores the resources just removed. Persist exclusions for every tool whose hooks remain, or otherwise prevent retained hooks from syncing until uninstall is retried.

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.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Disable the HTTP source when hook cleanup fails — src/local-agent.ts:3805: When teamai source remove-http cannot remove an agent hook from malformed settings, this branch retains the entire local-agent home, including its active endpoint configuration, and returns with exit code 0. Once the member repairs the settings file, the retained hook runs again and localAgentHandler reconnects to the HTTP source, potentially reinstalling the resources that were just removed. Preserve the hook ownership record separately while disabling the HTTP source, and report the removal as incomplete.

The previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Serialize HTTP source shutdown with in-flight hook syncs — src/local-agent.ts:3824: remove-http writes the disabled sentinel only after uninstalling resources and hooks, without locking against detached Stop/PostToolUse syncs. A sync that loaded the old config at src/local-agent.ts:3425 can concurrently reinstall resources after teardown or overwrite the sentinel via saveLocalAgentConfig() at src/local-agent.ts:3481, reactivating the endpoint after the command reports removal. Disable/revoke the source before teardown and prevent already-started syncs from writing or applying commands afterward.

All previously reported findings appear resolved. The PR description includes sufficient representative real-CLI validation.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Serialize full uninstall with the HTTP lifecycle lock — src/uninstall.ts:1598: source remove-http now waits for .local-agent-sync-lock, but teamai uninstall still removes agent hooks, tears down plugins, and deletes ~/.teamai without acquiring that lock or disabling the source first. If a detached Stop/PostToolUse sync is paused after loading the configuration, uninstall can report success before that sync resumes, processes an install_* command, and recreates resources and local-agent state. Use the same locked, disable-before-teardown path for full uninstall.

The earlier source remove-http concurrency finding is resolved for that command. The PR description provides sufficient representative real-CLI validation.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Serialize full uninstall with the HTTP lifecycle lock — src/uninstall.ts:1741: teamai uninstall still starts MCP/resource teardown without acquiring .local-agent-sync-lock or disabling the HTTP source. If a detached hook sync has already loaded the configuration, uninstall can report success and delete ~/.teamai, after which the sync resumes, processes an install_* command, and recreates resources and local-agent state. Use the same locked, disable-before-teardown lifecycle as source remove-http.

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.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
  • [P1 blocking] Avoid reacquiring the HTTP lock during server-pushed uninstall — src/uninstall.ts:1744: reportAndSyncLocalAgent() holds .local-agent-sync-lock while an uninstall_teamai command waits for its teamai uninstall --force --agent … subprocess. When that agent is the final user-scope tool, the subprocess enters this block, waits 30 seconds for the parent-held lock, then exits with failure. The supported remote uninstall is therefore acknowledged as failed and leaves the installation active. Defer the subprocess until the sync releases the lock or transfer/bypass lock ownership safely.

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.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

No findings.

  • The earlier lock re-acquisition issue is resolved: the server-pushed uninstall verifies its parent owns the lifecycle lock and does not release the inherited lock.
  • The PR description includes sufficient representative real-CLI and E2E validation for the runtime changes.
  • Per instruction, I reviewed the diff only and did not run or build PR code.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
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.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
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.
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 8, 2026
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.
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

No findings.

  • All previously reported issues are resolved in the current diff.
  • The PR description includes sufficient representative real-CLI and E2E validation. Later changes affect only documentation and tests, not runtime source.
  • Per instruction, I reviewed the diff only and did not run, build, or install PR code.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 9, 2026
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.
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

No findings.

  • All previously reported issues are resolved in the current diff.
  • The PR patch is unchanged from the head reviewed on October 9, 2026; the latest commit only merges the updated base branch.
  • The PR description includes sufficient representative real-CLI and E2E validation for its runtime changes.
  • Per instruction, I reviewed the diff only and did not run, build, or install PR code.

@jeff-r2026
jeff-r2026 merged commit d89c51e into Tencent:main Oct 9, 2026
14 checks passed
SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Oct 9, 2026
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.
jeff-r2026 pushed a commit that referenced this pull request Oct 9, 2026
…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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants