Repository navigation
Conversation
With any plugin configured, OpenCode's first request for a directory waits for it to npm-install @opencode-ai/plugin into its config dir. On CI that fetch outlasted the 5 s session request. teamai's plugins import nothing from it, so the fixture records it as installed, and a failed session request now carries the server log.
…as an edit (Tencent#993) judgeRemoval replaces removedCopyChanged with three outcomes: remove, edited (changed since teamai delivered it, or on another checkout's record), and notTeamais (no record, no team version). Pull's tombstone cleanup and the rules sweep now say a member's own file is not teamai's instead of claiming the member changed a teamai copy.
…eamai's (Tencent#993) removeStaleAgentSiblings deleted every same-stem file with another extension beside a rendered agent, a member's own included. It now goes through judgeRemoval with the agent's origin and the checkout record, and names a sibling it keeps. Built-in agents keep sweeping their own names.
…Tencent#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.
…e moves onto it (Tencent#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 Tencent#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.
…ng it (Tencent#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.
…t#993) Node 22 on macOS failed on 9309e20 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 Tencent#995 does not touch that code. The fork cannot re-run jobs, so this empty commit re-triggers CI.
…ncent#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.
…tExclude (Tencent#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).
…t#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.
…not rewrite (Tencent#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.
…all (Tencent#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 b62e503. Hook adoption needs no change: reconcileHooks saves its records whenever they differ from the previous ones, whether or not it wrote a file.
…mai owns (Tencent#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.
Tencent#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.
…eep Codex's shared skills (Tencent#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.
…ngs (Tencent#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.
…ut vitest's RPC Vitest's worker sends each task update with a 60s RPC timeout and reads the reply only when its event loop turns. Back-to-back synchronous tests never let it turn, so on a slow runner the run exits 1 with Timeout calling "onTaskUpdate" although every test passed.
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.
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's ubuntu runner has 4 vCPUs. On this branch, 4 workers took the fork-safe E2E job from about 24 to about 15 minutes (two runs of each setting); the slowest test went from 22s to 33s, under the 60s timeout.
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.
The five findings from the earlier pass are resolved in the current diff. The PR description includes sufficient real-CLI testing evidence for this runtime-changing PR. |
…de lines The OpenCode V2 plugin stopped at the nearest .opencode with a rules directory, so a member's own .opencode/rules in a nested package hid the teamai project above it. It now prefers a .opencode holding a teamai file and falls back to the nearest rules directory only when none does. A local-agent workspace whose flag cannot be read passed only the paths git could place to the sync, which then dropped the lines of the others. Those paths now reach the sync, which keeps every line while git fails.
The other five first-pass findings and the local-agent preservation finding are resolved. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
…lete - OpenCode V2 plugin: with no teamai file above the session, read every .opencode/rules up, outermost first, as V1's glob does, so a nested package's own rules no longer hide a rules-only team's. - Codex: a hook file git cannot judge inside a repository counts as not teamai's, and the project file keeps the team hooks unless the dispatcher entries were written to ~/.codex/hooks.json. - Uninstall: a delivered skill, rule, agent or docs file that could not be deleted keeps its exclude line and the data home, and marks the run incomplete so the retry finds it. - Local agent: the legacy .codebuddy/.gitignore goes only when git says it is untracked.
The previously reported findings are resolved in the current diff. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
…ecords on failed cleanup - gitUntracked: a file git says it does not track, or one in no repository; a repository git cannot answer for is neither. Delivered copies, a created models file and Codex's project hook file use it, so a git failure no longer lets teamai delete or take over a tracked file. - Uninstall: an instruction file whose block stays, or a docs search whitelist it cannot remove, keeps its exclude line and the data home, and the run exits 1. - Stopping Codex dispatch restores the project's index entry when ~/.codex/hooks.json cannot be updated, so the retry removes its entries.
The previously reported findings are resolved in the current diff. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
…lete Uninstall stops the project's Codex team-hook dispatch before the data home goes. When ~/.codex/hooks.json or the dispatcher index cannot be updated, the dispatcher counts as a hook left in place: the records stay for the retry and the command exits 1. Excluding only Codex from the project does the same.
The previously reported findings are resolved in the current diff. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
… not delete Removing the HTTP source logged a models file it could not clean, or a workspace cache it could not delete, then deleted the manifests that name them and reported success. The teardown now keeps those manifests, reports the removal incomplete with exit 1, and the retry, which finds them with the source already disabled, removes what is left. A user-scope `teamai uninstall` keeps the local agent's home when its teardown is incomplete.
The previously reported findings are resolved in the current diff. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
…moval could not handle - A skill, rule or instruction the teardown could not uninstall keeps its manifest entry. The disabled marker keeps the source config while any entry is left, so the retry uninstalls it. The run exits 1. - A model record that cannot be read or parsed is kept, no models file is changed, and the run exits 1, instead of being read as empty and deleted with teamai's models and keys still in place.
The previously reported findings remain resolved. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
… report kept exclude lines - A skill's cached source goes after its copies, so a copy that cannot be deleted fails the entry while the cache still proves which files are teamai's, and the retry removes it. - Removing the HTTP source counts an exclude file whose local-agent or credentials lines it could not write as incomplete, and exits 1. A retry finds a credentials record left behind and judges its lines from the checkouts of the recorded exclude files.
The previously reported findings are resolved. The PR description contains sufficient real-CLI testing evidence for this runtime-changing PR. |
…down would remove - The cache .gitignore a workspace teardown wrote stays, and fails the cache cleanup, while git cannot say whether the repository tracks it. - A skill or rule uninstall fails before it removes anything while git cannot judge a copy on disk, so its cache and records stay to prove the copy teamai's on the retry. - An unreadable or malformed git-exclude.json is kept, as it may name blocks still in place.
Summary
Closes #915. The ownership fixes from #995 are merged and included. The pending live tool checks must be verified before merge.
sharing.gitExclude.enableddefaults on for new teams and off for existing ones. A member can override it withgitExcludeEnabled. Credentials stay protected regardless of that setting. Member lines ininfo/excludestay intact. Tracked files remain visible and are never deleted by cleanup.Claude and CodeBuddy MCP servers move to their tools' local scopes. Copilot instructions use an owned file. Codex dispatches project team hooks through its user hook configuration. OpenCode V2 receives instructions and MCP through the plugin; V1 retains its supported file layout. The docs mirror, built-in resources, HTTP deliveries, and workspace cache records are covered.
Evidence
Before: a no-config HTTP workspace exposed
.teamai/.gitignoreand its MCP records togit status; project uninstall deleted an edited managed MCP server. The new real-CLI cases failed on those behaviors. After: generated cache files are ignored, member files remain addable, and edited servers stay with a warning.Workspace MCP cleanup:
teamai uninstall --forcein the current workspace reads its.teamai/managed-local-mcp.json, both without config and with user config. Invalid tool JSON preserves the records, names the file, and exits 1. Repairing the JSON and repeating the command removes the server and its plaintext token. The 10 MCP cases and 7 HTTP delivery cases pass with--retry=0.Validation at
7567e274:npm run build,npx tsc --noEmit, andnpm run lintpass. The completenpx vitest runpasses 8,352 tests, with 19 skipped. A Git discovery ceiling at the dependency directory keeps the real-shell fixture from inheriting its parent checkout; assertions are unchanged.Full real-CLI E2E:
npm run test:e2e -- --retry=0passes 983 tests across 124 files, with 27 tests in 3 files skipped. It exits 0 with no RPC timeout, taking 8 min 43 s locally with 4 workers. CI must confirm the new head.The common-directory probe regression failed before caching and passes after it. Failed probes are still retried. The existing damaged-repository git-exclude regression passes unchanged.
After integrating
mainat6f85cf85(fix(hooks): keep one owner for main-checkout team hooks across worktrees #1003), validation ate5d505b8passes build, typecheck, lint and the full unit suite (8,366 passed, 19 skipped). Six affected real-CLI E2E files pass all 69 tests with--retry=0, including all 30 shared-hook isolation cases plus Codex dispatch, self mode, worktree exclusion, and uninstall. The complete 983-test E2E run above predates this merge; only the affected files were rerun after it.Validation at
63e2727cpasses build, typecheck, lint and the full unit suite (8,377 passed, 19 skipped). Five real-CLI E2E files (local-agent-model-key-915,local-agent-teardown-915,local-agent-delivered-915,local-agent-mcp-local-915,git-exclude-uninstall-915) pass all 51 tests with--retry=0.642e6c5apassedcodex-team-hook-dispatcher-915.69942c45passedgit-exclude-delivered-915,instruction-targetsandopencode-versions-915. The later commits change only uninstall and HTTP-source removal. Each failure path below was confirmed failing before its fix:~/.codex/hooks.jsonremoved the team hooks from the project file. Anuninstalloruninstall --agent codexthat could not update that file exited 0. One version also dropped the project from the dispatcher index, so the retry left the dispatcher in place.source remove-httpexited 0 in five cases: a models file that did not parse, a model record that did not parse, a workspace cache it could not delete, a skill it could not uninstall, and a read-only exclude file holding a deleted models file's line. Each time it dropped the record of what was left, which could include teamai's model key. When a skill copy could not be deleted, its cache was already gone, so the retry took the copy for the member's and left it. In a repository git could not read, it also left the cache.gitignoreand a rule copy behind and dropped their records. An unreadablegit-exclude.jsonwas deleted..opencode/rules; and a local-agent workspace with an unreadable flag lost its lines when git could not read the repository.#995 gaps closed here
E2E runner
#915 adds 14 E2E files, about 1000s of test time, so the fork-safe job grew from 14 to 24 minutes. Two test-infra changes, no runtime code:
yield-between-tests.ts(setup file): Vitest's worker sends each task update with a 60s RPC timeout and reads the reply only when its event loop turns. Back-to-back synchronous tests (spawnSyncof the CLI) never let it turn, so a streak past 60s on a slow runner exits 1 withTimeout calling "onTaskUpdate"although every test passed. AnafterEachthat awaitssetImmediatecaps the streak at one test. Repro: three synchronous 25s tests exit 1 without it and 0 with it. On GitHub runners, this branch without it hit the timeout in 8 of 8 E2E jobs; with it, 0 of 8.maxWorkers: 4in CI (was 2): the ubuntu runner has 4 vCPUs. Two runs each on this branch: 2 workers took 24.1 and 25.6 min, 4 workers 12.5 and 14.5 min, all tests passing; the slowest test went from 22s to 33s, under the 60s timeout.Runtime probe overhead
Successful
gitCommonDirprobes are reused during one CLI process. Failed probes are retried, and checkout locations must remain stable for that process. OpenCode is probed for its version only after confirming that the current plugin exists. Git-exclude path routing remains uncached so a repository that stops answering is reported immediately.Two sequential runs per variant with
--maxWorkers=1on the same machine:claude-mcp-local-915git-exclude-worktrees-915These are local measurements for two files, not a measured reduction of the whole CI job.
Deviations and known limits
!/docs/**. Local MCP ownership is keyed by tool and local-scope key..mcp.json, listed while teamai-only. Self-mode worktrees share one Claude key, which cannot hold different servers for each branch.source remove-httpleaves project MCP servers and preserves their records. Runteamai uninstallin the affected workspace, or use the backend'suninstall_mcp. Uninstall does not scan every unconfigured workspace..opencodeholdingteamai-context.mdorteamai-mcp.json. Without one, it reads every.opencode/rulesfrom the session up, outermost first, as OpenCode V1 globs teamai's rules entry. A member's nested rules then load beside the team's..codebuddy/.gitignore.~/.codex/hooks.json. If uninstall cannot update that file or the dispatcher index, the project stays in the index and the data home is kept. The run exits 1, and the retry removes the dispatcher.source remove-httpmay fail to uninstall a skill, rule or instruction, to read its model record, to clean a models file, or to delete a workspace cache. The same holds when it cannot write an exclude file holding its lines. It then keeps the records naming them and exits 1. A skill's cache goes only after its copies, so a retry can still tell teamai's files from the member's. While git cannot judge a copy or the cache.gitignore, nothing is removed for it, and its records stay. An unreadablegit-exclude.jsonis kept. While installs remain, the disabled marker keeps the source config, which only the retry reads. The retry removes what is left, and a user-scopeteamai uninstallthen keeps the local agent's home.Kept <lines> in <file>, so git still ignores <paths>, which this uninstall leaves on disk.Live checks pending
Before merge: Cursor must load an excluded nested rule; Copilot must apply the owned instructions without an attached file; Codex must run context and blocking hooks through the dispatcher; OpenCode V2 must load plugin context and a project-only MCP server. Fixtures and steps are prepared, but these live checks are not verified.
Conditional changes remain deferred: claude-internal and tclaude need a
/memoryor/contextcheck provingrules/teamai-context.mdloads; Qoder and Qoder CN need confirmation thatsettings.local.jsonsupplies MCP configuration.Merge Danger
Door: two-way. Disabling the option removes delivery blocks on the next pull. Uninstall removes protection after safe cleanup and keeps records for failed removals.
Blast Radius: project delivery. Existing teams opt in; the first pull after upgrade runs a full sync. The pending live tool checks must complete before merge.