fix(pull): report hooks and MCP entry warnings on a dry run - #832
Conversation
`pull --dry-run` returned before the hooks and MCP reconcile stages, so the warnings those stages raise never reached the maintainer who ran the dry run to see exactly them: an unknown entry id, a per-entry `roles:` key, a hooks.yaml that does not parse. A dry run must resolve and warn, then skip the write (Tencent#822, item 3). MCP already had the capability — `McpReconcileOptions.dryRun` gates every write in mcp-reconcile.ts and `teamai mcp inject --dry-run` uses it — so `reconcileMcpAllScopes` only forwards it. Hooks had no dry-run path at all, so `reconcileTeamHooksForConfig` gained one: it resolves the entries, reports what it found, and stops before the first write. `resolveTeamHooks` takes a `preview` flag so the transparency line reads "Would apply N team hook(s)" instead of claiming they were applied. The tests drive `pull()` rather than the reconcile functions, the way pull-env-shape-warning.test.ts does: the defect was in the orchestration layer, so that is where it has to be pinned. They cover the dry-run warning, the zero writes, the unchanged real-pull behavior, and the dryRun forwarding.
|
Checked for overlap with the open PRs that touch the same files:
Happy to rebase on either once they land. |
|
Added a real-CLI check, since the unit tests drive Built
After it, the sandbox HOME held only the pre-existing
So the dry run reports exactly what a real pull would say, with |
…ey (Tencent#822) Item 1. Env, hook and MCP entry schemas are plain z.object, which strips unknown keys, so a mistyped scoping key (`role:` for `roles:`) vanished and the entry reached every member. Each reader now reports the keys an entry was written with that its schema does not know (known keys come from the schema's own shape), and keepScopedEntry does not deliver such an entry and warns once, naming the file, the entry and the key, the same path the removed `projects:` key takes. doctor's per-entry-key check is retitled to cover it. `env add`/`env remove` and `remove mcp` keep such a key when they rewrite the file; `remove mcp` edits the YAML document instead of re-serializing the parsed servers. Item 4. recall ended every result with a Chinese line; it is English now. Item 2 is not a bug: tags reaching a tagged skill in an inactive namespace is the behavior Tencent#337 added and roles-tags-pull tests. The design doc's Known gaps entry now says so. Item 3 (pull --dry-run warnings) is left to Tencent#832.
|
Findings
|
jeff-r2026
left a comment
There was a problem hiding this comment.
Please resolve the P1 findings.
… doc Review follow-up: with dryRun forwarded, the MCP summary still read as a completed apply — "N change(s) ... Restart your AI tool session to load them" after a run that wrote nothing. A dry run now reads "Would make N change(s)" and omits the restart instruction; a real pull keeps the applied wording. docs/designs/multi-project-management.md still said `pull --dry-run` prints no hooks or MCP warnings; it now describes the resolve-and-warn behavior this PR ships. The new tests pin the wording on each side of the dry-run boundary: the dry-run case failed on the previous commit, the real-pull case passed.
|
Findings
Resolved
|
The [scope] "Reconciled N team hook(s)" debug line still claimed a reconcile after a run that wrote nothing. It now reads "Would apply N team hook(s)" on a dry run and keeps "Reconciled" on a real pull; both wordings are pinned by the dry-run tests.
|
All findings are resolved on
The full suite still exceeds ~13 minutes on this Windows machine, so the authoritative full-suite signal remains CI (green on ubuntu/macos × Node 20/22); the PR description documents the representative real-CLI run the review rules ask for. #814 landed while this PR was open; the regions are disjoint and GitHub reports the branch mergeable, so I have not rebased — happy to if you prefer. |
|
Findings
Resolved
|
The per-entry `tools: [pi]` case: a real pull warns "Pi supports built-in lifecycle hooks only; skipping N custom team hook(s)" during the per-tool pass, but the dry run returned before that pass, so its "Would apply" line promised hooks no tool will run. Extract the Pi report from the loop so both paths print the same warning — the dry run gated on the same toolPaths and agent filters the real pass uses — and pin it with a test that is RED without the fix.
|
The Pi finding is fixed on Also verified the full-suite question from the description with a baseline run: 201 failed / 4770 passed on the merge-base ( |
|
Findings
Resolved
Testing
|
…ey (Tencent#822) Item 1. Env, hook and MCP entry schemas are plain z.object, which strips unknown keys, so a mistyped scoping key (`role:` for `roles:`) vanished and the entry reached every member. Each reader now reports the keys an entry was written with that its schema does not know (known keys come from the schema's own shape), and keepScopedEntry does not deliver such an entry and warns once, naming the file, the entry and the key, the same path the removed `projects:` key takes. doctor's per-entry-key check is retitled to cover it. `env add`/`env remove` and `remove mcp` keep such a key when they rewrite the file; `remove mcp` edits the YAML document instead of re-serializing the parsed servers. Item 4. recall ended every result with a Chinese line; it is English now. Item 2 is not a bug: tags reaching a tagged skill in an inactive namespace is the behavior Tencent#337 added and roles-tags-pull tests. The design doc's Known gaps entry now says so. Item 3 (pull --dry-run warnings) is left to Tencent#832.
…ey (#822) (#833) * fix(pull): do not deliver an env, hook or MCP entry with a mistyped key (#822) Item 1. Env, hook and MCP entry schemas are plain z.object, which strips unknown keys, so a mistyped scoping key (`role:` for `roles:`) vanished and the entry reached every member. Each reader now reports the keys an entry was written with that its schema does not know (known keys come from the schema's own shape), and keepScopedEntry does not deliver such an entry and warns once, naming the file, the entry and the key, the same path the removed `projects:` key takes. doctor's per-entry-key check is retitled to cover it. `env add`/`env remove` and `remove mcp` keep such a key when they rewrite the file; `remove mcp` edits the YAML document instead of re-serializing the parsed servers. Item 4. recall ended every result with a Chinese line; it is English now. Item 2 is not a bug: tags reaching a tagged skill in an inactive namespace is the behavior #337 added and roles-tags-pull tests. The design doc's Known gaps entry now says so. Item 3 (pull --dry-run warnings) is left to #832. * fix(env): warn when env add updates a variable pull does not deliver (#822) Updating a variable that carries an unknown key keeps the key, so the variable stays undelivered; env add now says so instead of only reporting 'Updated env variable'. * fix(pull): keep installed MCP servers and hooks when their file has no known top-level key (#822) A hooks or MCP file with `server:` for `servers:` parsed as empty and removed every installed team server or hook for every member, silently. Such a file now fails like one that does not parse, naming the keys found and the key expected. An extra key beside a known one is still ignored.
What
pull --dry-runreturned before the hooks and MCP reconcile stages, so the warnings those stages raise never reached the maintainer who ran the dry run to see exactly them (#822, item 3):A dry run now resolves the entries and reports what it finds, then skips the writes. The MCP summary line matches: on a dry run it reads
Would make N change(s)…and omits theRestart your AI tool sessioninstruction, since nothing was written for a session to load.Why
The warnings come from the entry resolution, which is read-only:
resolveEntriesFor→reportEntryResolution(unknown ids, a deprecated per-entryroles:, ahooks.yamlthat does not parse). Only the later reconcile writes. Returning early skipped the resolve, which is the half that carries the information a maintainer wants from--dry-run.How
MCP —
McpReconcileOptions.dryRunalready gates every write inmcp-reconcile.ts, andteamai mcp inject --dry-runuses it.reconcileMcpAllScopesonly forwards it; no new mechanism. Its summary line branches on the dry run:Would make N change(s) across M server(s)when nothing was written, the applied wording with the session-restart hint on a real pull.hooks — had no dry-run path at all.
reconcileTeamHooksForConfigtakesdryRun, resolves the entries, reports them, and returns before the first write (reconcileHooksToAllTools, the copilot branch, andsweepLegacyProjectHooks).resolveTeamHookstakespreviewso the transparency line reads "Would apply N team hook(s)" rather than claiming they were applied. One report happens inside the per-tool pass, so the dry run repeats it before stopping: Pi cannot run custom team hooks, and atools: [pi]hook must draw the same "Pi supports built-in lifecycle hooks only; skipping N custom team hook(s)" warning a real pull prints.docs —
docs/designs/multi-project-management.mdno longer says thatpull --dry-runprints no hooks or MCP warnings; it now describes the resolve-and-warn behavior this PR ships.reconcileCoAuthorAllScopeskeeps its early return: it is write-only with no warnings to report, so a dry run has nothing to show there.Tests
src/__tests__/pull-dry-run-hooks-mcp.test.tsdrivespull()the waypull-env-shape-warning.test.tsdoes, since the defect was in the orchestration layer:roles:key on a dry runsaveStateForScopealso untouched)dryRunto the MCP reconcileWould make …, no restart line) — RED on the previous headRED first: the dry-run cases failed before their fixes (the MCP-summary one on the previous head), the behavior-preserving cases passed.
Real-CLI verification (
ea39829)npm run build, thennode dist/index.jsagainst a sandboxHOMEand a local team-repo fixture whosehooks/hooks.yamlcarries the deprecated per-entryroles:key and whosemcp/mcp.yamldeclares one stdio server.pull --dry-run --force— warns, previews, writes nothing:Afterwards the sandbox HOME held only the pre-existing
config.yamlanddebug.log— no.claude/settings.json, no.claude.json, no.teamai/managed-mcp.json.pull --forceon the same fixture — same warning, applied wording, writes:Verified locally:
vitest run src/__tests__/pull-dry-run-hooks-mcp.test.ts→ 7 passed;tsc --noEmit→ exit 0. The full suite takes ~13.5 minutes on this Windows machine and fails the same way on the merge-base as on this branch, so I ran it on both: 201 failed / 4770 passed on87a606b, 198 failed / 4779 passed on this head — the branch adds 7 tests and introduces no new local failure. The failures are Windows-environment classes on both sides (EPERM symlinks without privilege, POSIX path-separator and0o600file-mode assertions,npmENOENT inspawnSync, git CRLF churn, 15s timeouts on git-heavy tests); the same suite is green in CI on ubuntu/macos × Node 20/22.