Repository navigation
feat(locate): rules, blocks, agents, and MCP go through locate (#1221) - #1265
Conversation
Tencent#1221) locate now answers with a channel ('files', 'run-time', 'none', 'unknown') and places that say what kind they are: a directory, a blocks file, or a map inside a shared config file. A locator per resource sits in LOCATORS, so a resource without one fails tsc, and each resource keeps its own result type. Skills is still the only resource. The helpers serve every place kind: writable (was writableDir) and ownOf replace the 12 writableDir calls and 4 reads of .own; writesOf, cleanupOf and unreadWrites also handle files, map entries and a read hidden by an earlier file of a first-existing lookup ('shadowed'). No behavior change: A2's skills matrix and locate tests pass with only the accessor adapted to places.
…into Install (Tencent#1221) installOf now also reads sharing.gitExclude, whether the business repo is the team repo, and whether the workspace has child checkouts, once, so the rules, blocks and MCP locators get them from Install instead of each caller. Host gains realpath and readText, the two reads the key Claude Code files a checkout under needs (Tencent#915). Nothing reads the new fields yet.
… module (Tencent#1221) codex-project-trust.ts now holds Codex's verdict on a project and the fix doctor prints, taking the ReadGate locate declares, so the agents and MCP checks can share one reading. Doctor's MCP trust check builds the same gate and prints the same lines.
…cent#1221) The flag now says what still goes to the default home when OpenCode, Pi or Muse moves its home: teamai's hooks and models switch settings, until B4 moves them.
…s, not skillDirs (Tencent#1221) skill-dirs.ts becomes places.ts: lastPullByWorkspace[key].places holds, per resource, the places each tool took it from at the last full pull. Skills is the only resource recorded; rules, blocks and agents join once their writers go through locate. The fast path compares every recorded resource (placesUnmoved). A record from an earlier release reads its skillDirs as places.skills, and pull never writes skillDirs again.
…s in the descriptor (Tencent#1221) Each built-in tool now names, per scope, how it takes rules, instruction blocks, agents and MCP servers: the adapter with the paths it reads, a run-time channel with the probe that says it is installed, 'none', or 'unknown'. Leaving a resource or a scope out fails tsc (tools.type-test.ts). The rows hold the Tencent#1221 design, including WorkBuddy's MCP lookup (D-e), ZCode's mcp.servers (D-c) and no MCP channel for the internal builds (D-f); no writer reads them yet. The per-tool tables that decided the same facts are now views of the descriptor: the rule formats' tool keys, the session-hook and extension rule tools, detectMcpFormat's sets and the agent tool list. A test pins them to the tables they replaced. The internal builds keep their MCP format until the MCP writers move to locate.
…Tencent#1221) Every configured or declared tool path now becomes absolute in rootsOf, and every resource takes one write gate per tool (gatesOf), so the blocks file, OpenCode's activation and the MCP file resolve once and move with a home. No caller uses the new locators yet; their tables run through one shared matrix runner, which the skills matrix now uses too.
…OLS, not ALL_SUPPORTED_TOOLS (Tencent#1221) ALL_SUPPORTED_TOOLS was a copy of AGENT_TOOLS read through casts. Its readers narrow with isAgentToolId instead, and the copy is gone.
…ncent#1221) One row per tool and scope, plus the moved homes: OpenCode under XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR and a recorded home, Muse under XDG_CONFIG_HOME, Copilot under COPILOT_HOME, and Claude's toolRoots.
…ent#1221) Every tool in both scopes, each row citing its source. The dsh workspace cells follow Tencent#1223: dsh loads teamai's hook config, so its SessionStart hook carries the workspace rules and blocks.
…encent#1221) resolveMcpTargets now takes every target from locate(tool, 'mcp'): WorkBuddy's user servers go to the first existing of its lookup order, its project servers to CodeBuddy's local scope under sharing.gitExclude, and OpenCode, Muse and Pi follow a moved home (XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR, PI_CODING_AGENT_DIR). The local-scope tools, the file readers of claude's .mcp.json and OpenCode's plugin file come from the descriptor instead of tables here. Copilot's MCP file still says it is installed (Tencent#628).
…ds them (Tencent#1221) install_mcp and uninstall_mcp take the tool's MCP file from locate(tool, 'mcp'), as a pull does: WorkBuddy's user servers go to the first of its files that exists, and OpenCode, Muse and Pi follow a moved home. USER_MCP_LOOKUP and userMcpFile go; doctor's read-file check names the target's own lookup files.
…d home (Tencent#1221) Every rules directory pull, push, the pre-push sync, the built-in rules, recall disable, the Cursor user-rules hook, doctor and the local agent's report read now comes from locate(tool, 'rules'). OpenCode under XDG_CONFIG_HOME or OPENCODE_CONFIG_DIR gets its rules and their `instructions` glob in the config dir it reads, and the glob goes to the activation file locate names instead of the MCP field. Muse under XDG_CONFIG_HOME and Pi under PI_CODING_AGENT_DIR get the team-rules block in the AGENTS.md they read. The rules locator keeps honoring a team toolPaths entry written before a tool's rules moved (Tencent#938, Tencent#946): it names a rules directory the tool never reads, so locate returns that directory with no reads, pull still writes there and doctor still fails it. The inline team-rules block comes from the blocks location, so such an entry keeps its block too.
…encent#1221) agentToolDirs, which the pull writer, doctor and the model holds read, asks locate. OpenCode and Muse agents now follow a home that XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR or the tool-homes record moves, as their skills already did.
…Tencent#1221) deployBuiltinAgents and recall off ask locate, so the recall agent follows a moved OpenCode or Muse home too. deployBuiltinAgents now takes the local config: every caller passes one, and the test that called it without one passes the member's config instead.
…ai renders none for (Tencent#1221) An earlier release copied team .md agents into any configured agents directory, so teamai remove, the tombstone pass and uninstall still clear its copies there.
…encent#1221) The push scan and teamai remove ask locate for each tool's agents directory, so they see the copies in a moved OpenCode or Muse home. A custom tool, for which teamai renders no agents, is no longer scanned or cleaned.
… it (Tencent#1221) runLocalDriver's toolPaths are now the team's as teamai.yaml states them, and loadAgent asks locate, so a user-scope agent in a moved OpenCode home loads.
A custom tool gets no agents directory: teamai renders no agents for it.
…y it left (Tencent#1221) places.agents joins the record the fast path compares, so a home that moves at one team revision syncs agents in full, and the first pull after it names the old directory once. A tool that did not take every agent (a member's own file, a held model, a failed write or built-in) keeps its old directory on record until it does. The record of an earlier release names no agents places, so its first pull syncs in full: the skillDirs upgrade test now expects that, and the fast-path fixtures record agents places as this CLI does.
…sts the project that holds them (Tencent#1221) Doctor names a tool whose agents moved since the last pull, notes a tool whose agents follow a moved home while its other files stay in the default one, and checks Codex trust for a team that ships agents and no MCP servers.
…Tencent#1221) The agents rows of locate-agents.test.ts state each of these paths with its source: OMP's user agents, Qoder CN's user agents, WorkBuddy's default, and Copilot's project directory. The whole default toolPaths literals and the toolRoots helper outputs stay, since they pin every other key too.
) Every MCP writer now records the file it wrote a server to: pull, which `mcp inject` and `mcp remove` run, and the local agent. A record whose file the tool no longer reads splits three ways: - in one of teamai's own relocations (a lookup file, the mapped or alternate file, an earlier release's default): moved out, as before; - still holding the server elsewhere, after a home move or a remap: kept, and named once, as skills are; - anywhere else: taken as the current file's. A record an older teamai wrote notes no file. It reads as the mapped file for a lookup target, the default home for OpenCode, Muse and Pi, and the target's file otherwise; one whose entry an earlier release's default holds unchanged is moved out of that file. locate marks the built-in path of a remapped tool 'builtin', apart from an earlier release's 'default': a remap leaves the old file as it is.
… installed (Tencent#1221) instructionHookChannel answered ready for every tool it did not know, so a ZCode, DeepSeek Harness or Muse Code channel with no teamai hook counted as delivering. channelReadiness takes the probe the tool's run-time channel names and checks it: the SessionStart hook in the hooks file for Codex and Muse Code, that hook plus hooks.enabled for ZCode, the cordis.patch.yml row and the bridge config for DeepSeek Harness. A new probe fails the type check until it has a check.
…1221) Uninstall takes the servers of a partition that is no project out of each local scope where locate places it, so WorkBuddy's in .codebuddy.json go too, and CODEBUDDY_CONFIG_DIR is read through the host. The files whose git exclusions it removes are every place cleanupOf names for a tool: its file, OpenCode's plugin file, the other files of a lookup and the retired ones.
…cks at session start (Tencent#1221) In a project these three read no file teamai could write the culture, claudemd and recall blocks to, and their session-start hook added only the rules, so they got none of the blocks. Whether a tool takes a scope's blocks from a hook now comes from its run-time blocks channel in the descriptor: the hook adds the blocks before the rules, the HTTP agent's prompts ride the same hook and are written to no file, and the hook's limit comes from the descriptor. project-hook-rules.test.ts pinned "rules only, no instruction blocks" for the three: that was Tencent#945's requirement, and Tencent#1221 replaces it, so the test asserts the blocks now. A run-time channel is probed in the member's home, where the hook or extension lives; the instruction-targets shape test creates ~/.pi for Pi as it already did ~/.omp.
…1221) readJsonDoc and writeJsonDoc take the keys from a file's top to its server map, keeping every other key on the way, and a target carries the key locate names when it is not the format's own. No target has one yet; ZCode's `mcp.servers` is next. The local agent writes under the key of the place it writes.
…ne is in, in the scope a project's servers left too (Tencent#1221)
Testing: The description documents build/check results and representative real-CLI verification, including a run on Earlier findings: The missing-history issue and all previously reported code scenarios are resolved in the current diff. Review was read-only; no PR code was run, built, or installed. |
…le, and keep what a kept record says of resolved values (Tencent#1221)
Testing: The description documents build/check results and representative real-CLI verification on Earlier findings: The missing-history issue and all previously reported code scenarios are resolved in the current diff. Review was read-only; no PR code was run, built, or installed. |
… it may sit in cannot be read (Tencent#1221)
…'s servers does not parse (Tencent#1221)
Testing: The description supplies build/check results and representative real-CLI verification on Earlier findings: The missing-history issue and all previously reported code scenarios are resolved. Review was read-only; no PR code was run, built, or installed. |
…vers sit in a config that does not parse (Tencent#1221)
|
No new findings I’m confident are real in the current diff.
Reviewed only |
Resolve the doctor-delivery.ts import conflict with Tencent#1274 and update its dropped-keys fixture to the zcode default this branch sets (mcp in .zcode/cli/config.json, mcpProject in .zcode/config.json).
|
No new findings I’m confident are real in the current diff.
Reviewed only |
# Conflicts: # src/teamai-only-files.ts
|
No new findings I’m confident are real in the current diff.
Reviewed only |
jeff-r2026
left a comment
There was a problem hiding this comment.
Reviewed at f2faef2 merged with current main (no conflicts): tsc, lint, build pass; full unit suite passes except 2 cases in submodule-checkout.test.ts that fail identically on plain main; e2e mcp-uninstall / codebuddy-mcp-local-915 / teamai-only-files-915 / doctor-delivery-cli 48/48. Real-CLI smoke with OpenCode home moved via XDG_CONFIG_HOME: pull now delivers rules/agents/MCP/instructions there (main skips them), doctor green, uninstall --force cleans everything.
Non-blocking, please call out in release notes: (1) a team mapping claudemd for JoyCode/Kiro/Qoder/Trae is now treated as retired, so pull strips the teamai block there; (2) CODEBUDDY_CONFIG_DIR now moves the whole CodeBuddy home while hooks follow only in B4.
Summary
This PR is steps B1, B2 and B3 of #1221.
flowchart LR subgraph callers ["callers"] rules["rules, blocks:<br/>pull, uninstall, doctor"]:::added agents["agents:<br/>pull, doctor, local agent"]:::added mcp["MCP:<br/>resolveMcpTargets, doctor"]:::added skills["skills (#1252)"] later["hooks, self scope,<br/>models switch settings"]:::todo end subgraph core ["src/locate.ts"] locate["locate(tool,<br/>rules | blocks | agents | mcp)"]:::added places["places records<br/>skills, rules, blocks, agents"]:::added end host{{"Host"}} own["MCP file stays on<br/>the ownership record"] left["name a place the tool left<br/>copies stay"]:::added page["runtime rows and<br/>docs/guide/tool-support.md"]:::todo old["path joins,<br/>instructionTargetPath"]:::removed skills --> locate rules & agents & mcp --> locate locate --> places locate --> host mcp --> own rules & agents --> left later -.-> locate places -.-> page rules & agents & mcp -.-> old classDef added fill:#2f8f4f,stroke:#1f6b39,color:#fff; classDef todo fill:#5c6670,stroke:#3d454c,color:#fff,stroke-dasharray:4 3; classDef removed fill:#a33a3a,stroke:#7a2a2a,color:#fff,stroke-dasharray:4 3;Green: this PR. Gray: later steps of #1221 (B4, then C and D). Red: removed.
Rules, instruction blocks, agents, and MCP servers are written where
locatesays each tool reads them, including a moved home. A place a tool left is named once. MCP does not join that record; uninstall still removes teamai's unchanged servers from a file the tool left.Two rules hold across every path:
config.tomlis edited as TOML, and a copy the member changed stays, as a JSON entry does.applyMcpFileinmcp-reconcile.tstoolRootsfollows the user config's, asresolveMemberToolRootsdoes. That covers everythinglocatereads from the tool's user config in project scope: the Codex hook gate of rules and blocks, Codex's trust config for agents and MCP, and Claude's local MCP scope. Project skills read no user-scope path.userViewOfinlocate.tsMCP ownership records that name a file other than the current one are cleaned in the file they name, for every tool, format and scope. One function,
applyMcpFileThenOthersinmcp-reconcile.ts, reconciles the current file and then those files. The reconcile loop calls it, and so does the cleanup of the place Claude's and CodeBuddy's project servers left (.mcp.jsonor the tool's local scope, assharing.gitExcludepicks):On uninstall, the current file's state (written, unchanged, missing, unparsable, unreadable) changes no row. A pull that cannot read the current file moves nothing and keeps every record. A file a pull moves servers out of on purpose (OpenCode's other project file, a lookup or retired file) keeps them, and their records, while it does not parse or cannot be read. A record from an earlier release that notes no file follows the current file when the file it guesses lacks the entry, and stays tied to a retired place that cannot be read, so the next run retries there. A record kept for a file the run could not read keeps what it said of resolved values. While a kept record's file cannot be read, uninstall keeps the data home that holds the record, names the file and exits 1, as it does for other files it could not clean. That covers every record a retry reaches: the checkout's own, and the local scope of every checkout of the project for each tool it syncs, a removed worktree's included. Those other checkouts' records also count as owned, so uninstall from a checkout that holds none of teamai's servers still cleans them.
A place is a file, a key in it, and a project under that key. A team config from before this PR can map ZCode's
mcpto~/.agents/mcp.json, where an earlier release wrote undermcpServerswhile ZCode now readsmcp.servers: those are two places in one file, and the retired one follows the table above. Every comparison of MCP places (locate, the record split, the other-file cleanup, the local agent, doctor) uses that identity.A
claudemdthe team maps for a tool with no blocks channel (JoyCode, Kiro, Qoder, Trae) is retired: pull strips teamai's blocks from it and uninstall finds them. ZCode, DeepSeek Harness and Muse Code get a project's blocks from the session-start hook.CODEBUDDY_CONFIG_DIRmoves CodeBuddy's whole home, not only.codebuddy.json. teamai now treats it as a home move and records it like the other ones, so every shell writes to the same place. Hooks stay in~/.codebuddy/settings.jsonuntil B4, and doctor says so.The README support table does not change. Every tick still matches the tool's channels here: this PR fixes where existing deliveries land, and no tool gains or loses a resource.
builtin-hooks.tskeeps the Codex team-hook command on barebash. That is a CI fix after mergingmain, not part of #1221.Evidence
toolPaths.rules,toolPaths.claudemd, and MCP paths themselves. A moved OpenCode or Muse home left rules and blocks at the old home.After: on
7222f4be(main merged in),npx tsc --noEmitandnpm run lintexited 0, andnpx vitest runpassed 503 files (1 skipped) and 14637 tests (21 skipped). Oncfc243a0,npm run buildexited 0. The MCP record table inmcp-locate-targets.test.tsruns 132 rows (mode x current-file state x recorded-file state, for OpenCode, Claude and Codex in both scopes), plus 16 for the place Claude's and CodeBuddy's project servers left, plus 8 for ZCode'smcpServers(uninstall and pull; same file or ZCode's own; the retired file unparsable or unreadable, then repaired). The 4 retired-file rows failed onee998ef8.e2e/mcp-uninstall.test.tsadds 2 rows (Claude's~/.claude.json, ZCode's retired~/.agents/mcp.json, each unparsable, then repaired); both failed onee998ef8.e2e/codebuddy-mcp-local-915.test.tsadds 2 rows: uninstall from a checkout holding no servers, while a removed worktree's servers sit in a.codebuddy.jsonthat parses, or does not parse until it is repaired; both failed on924ec0ba. Locally, the e2e suite passed 168 files and 1415 tests; the only failures were the two Ctrl-C cases ofworkflow-signals.test.ts, which fail only on this machine and touch no code here.cfc243a0, project scope, CodeBuddy,sharing.gitExcludeon:initandpullin the main checkout and in a linked worktree, then the team drops its servers and the main checkout pulls (its local scope is empty), the worktree is removed, and~/.codebuddy.json(still holdingplain-apifor the worktree) is made invalid JSON.uninstall --forceexits 1 withUninstall incomplete: kept <home>/.teamai/projects/<app> ... Repair permissions or JSON in <home>/.codebuddy.json (projects["<wt>"]), andmanaged-local-mcp.jsonkeeps the worktree's key. After the repair,uninstall --forceprintsRemoved 1 teamai-managed MCP server(s), exits 0, and the worktree's servers and the records are gone. On924ec0bathe first uninstall exited 0 and deleted the records, leavingplain-apiunowned.teamai init --agent codebuddy --scope projectwithsharing.gitExcludeon,teamai pull(~/.codebuddy.jsonholdsapifor the project; the member addsmine), thensharing.gitExcludeoff in the team config andCODEBUDDY_CONFIG_DIR=<dir>whose.codebuddy.jsonholds invalid JSON, with no pull, thenteamai uninstall --force:f86645eb:Could not parse <dir>/.codebuddy.json.~/.codebuddy.jsonstill holdsapi,mine.a488a3d4: the same warning, thenRemoved 1 teamai-managed MCP server(s).~/.codebuddy.jsonholdsmine, and<dir>/.codebuddy.jsonis untouched.3787ad14, user scope, OpenCode with no teammcpmapping,initthenpull:opencode.jsonliststeamai-context.mdininstructions, andteamai doctorpasses every OpenCode check. CodeBuddy 2.168.0 withCODEBUDDY_CONFIG_DIR=<dir>: skills, rules and.mcp.jsonland in<dir>, andcodebuddy mcp listshowsteamapi.Merge Danger
Door: two-way
A checkout whose places record has skills but not rules, blocks, or agents takes one full sync. The next pull can take the fast path again.
Blast Radius: pull, uninstall, local agent MCP
Doctor, uninstall, and the local agent read the same places. MCP cleanup still follows the ownership record. A local-scope MCP record an earlier release wrote without a file resolves in that scope, so the local agent can update it.
Uninstall now keeps a Codex MCP block the member changed since teamai wrote it, as it already did for JSON configs. Uninstall now exits 1 and keeps
~/.teamaiwhile an MCP config holding teamai's recorded servers does not parse or cannot be read; it used to delete the records and leave those servers unowned. That includes a removed worktree's local scope.A member with
CODEBUDDY_CONFIG_DIRset gets CodeBuddy's skills, rules, blocks, agents and MCP files in that directory from the next pull. The copies under~/.codebuddystay, and pull names that place once. Uninstall removes teamai's unchanged MCP servers from the old file.