Skip to content

fix(pull): gate the generic sync report on a tool that can receive it - #751

Merged
jeff-r2026 merged 2 commits into
Tencent:mainfrom
ydflow:fix/sync-count-truth
Sep 23, 2026
Merged

jeff-r2026 merged 2 commits into
Tencent:mainfrom
ydflow:fix/sync-count-truth

Conversation

@ydflow

@ydflow ydflow commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The generic branch of the sync loop reports the team repo's item count for every resource type. Skills and agents are written per tool into that tool's own directory, and a brand-new member has none of those yet — the handler skips such a tool by design and only logs at debug. So the first pull after init prints Synced N skills / Synced N agents while nothing reached the disk.

 pull, generic branch (skills / agents)
-  Synced ${items.length} ${type}          // what the team repo holds
+  Synced ${items.length} ${type}          // only when a tool can receive it

This is the phantom-success half of #585. #597 fixed the same shape for rules and explicitly left this branch open as "a known related gap rather than expanding this PR further"; an earlier commit in this branch closed it for skills. This one closes it for agents, which had the identical bug one resource type over.

What changed

getInstalledResourceTargets now takes an optional field. Without it the behaviour is unchanged ("any of skills/rules/agents"); with it, the walk is restricted to that one resource type. The generic branch calls it for skills and agents and gates the report on the result:

const needsToolRoot = type === 'skills' || type === 'agents';
const canReceive = !needsToolRoot
  || (await getInstalledResourceTargets(freshConfig, localConfig, type)).length > 0;

The previous hasInstalledTargetFor helper was a second copy of the same walk. It is gone: reporting and writing now share one resolver, so they cannot drift. That was the concrete hazard the review named — an external HERMES_HOME could receive the skill while the success line was suppressed, and an existing ~/.openclaw without a resolvable workspace could still print success while writing nothing.

Docs, rules and env never reach this branch (docs is handled above it, rules/env continue earlier). hooks and mcp have no toolPaths field to probe, so they keep reporting unconditionally.

TypeScript narrows type to 'skills' | 'hooks' | 'agents' | 'mcp' at this point, which is why the condition reads type === 'skills' || type === 'agents' rather than listing every ungated type.

Test Plan

Unit tests

src/__tests__/pull-sync-truth.test.ts drives the real pull() with the git provider, logger and reporting mocked, and seeds a team repo holding a skill, an agent and a doc:

  • no tool directory → no Synced N skills line, no Synced N agents line, and neither file is on disk; the docs line is still there and docs/guide.md really landed
  • tool directory present → both lines are back and both files are on disk
src/__tests__/pull-sync-truth.test.ts              4 passed
pull-scope-isolation.test.ts
pull-agents-cleanup.test.ts
pull-skip-sync.test.ts                             48 passed (4 files)

The RED is real: reverting only the agents half of the gate (needsToolRoot = type === 'skills') fails the new case with the phantom success itself, not a mock artifact:

expected [ '[user] Synced 1 agents' ] to deeply equal []

npx tsc --noEmit exit 0. npm run build exit 0.

End-to-end, real CLI

npm run build, then the compiled dist/index.js against a local git team repo (provider: git, --self-style single-repo layout with resources under .teamai/), isolated HOME, one skill and one agent, toolPaths.codex = { skills, rules, agents }.

Codex root absent — the gate suppresses the claim and the run surfaces the failed check instead:

$ teamai pull --force
- [project] Pulling team repo...
✔ [project] Team repo: single-repo (knowledge on main)
⚠ Pull finished, but 1 check(s) failed:
⚠   ✖ codex is installed
    → enabledAgents lists codex, but it has no directory under …/e2e754,
      so a pull delivers nothing to it. …

Codex root present — the claim returns:

$ mkdir .codex && teamai pull --force
✔ [project] Synced 1 agents

For contrast, the same first scenario on the pre-fix build still prints Synced 1 agents with nothing on disk — that is the bug this PR removes.

Not verified

I did not exercise a live teamai pull against a real remote, and I did not run npm run test:e2e — it needs TEAMAI_TEST_TOKEN and TEAMAI_TEST_REPO_URL, which I do not have. The e2e above is against a local git repo, which is the closest I could get. The full agent/provider matrix from AGENTS.md (Claude, Codex, CodeBuddy, OpenCode × git, gitlab, github) is therefore not covered by this record.

Related Issues

Fixes part of #585. Follows #597 (rules half) and the skills half of this same gap.

Notes for Reviewers

While building the e2e above I hit a separate, still-open defect that this PR does not fix and that I did not want to smuggle in: with the tool root present, the CLI can print Synced 1 agents while writing no agent file and logging nothing about agents at all — not even the usual Skipping agent sync for <tool>: tool not installed debug line. The gate above is about "can any tool receive this", so a root that exists is enough to pass it; the write itself is decided further down. Worth its own issue/PR rather than widening this one.

The generic branch of the sync loop reported the team repo's item count
for every resource type: `Synced N skills` counted what the repo holds,
not what landed. Skills are written per tool into that tool's own
directory, and a brand-new member has none of them yet — the handler
skips such a tool by design and only logs at debug. So the first pull
after `init` printed a success while nothing was on disk, which is the
phantom-success half of Tencent#585.

`hasInstalledTargetFor` asks the same question `getInstalledResourceTargets`
already asks, for one resource field, and the generic branch now gates
its report on it. Docs need no gate: they are copied to the team's own
docs directory, which the copy creates, so that report was already
truthful.

Only the report is gated. The writes still run, so a tool root created
later — Cursor makes `.cursor/` on first launch — is filled by the next
pull, and `pull --force` fills it now.

Fixes Tencent#585 (the generic branch). Tencent#597 fixed the same shape for rules
and left this one open; this closes it for skills.
@jeff-r2026 jeff-r2026 self-assigned this Sep 23, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/pull.ts:596 duplicates the skills installation check instead of using the handler’s real destination resolver. This disagrees for special tools: an external HERMES_HOME can receive the skill while the success line is suppressed, and an existing ~/.openclaw without a resolvable workspace can still print success while writing nothing. Use skillsDirForTool or SkillsHandler.deliveryTargets() so reporting and writing share one gate.
  • [P1 blocking] src/pull.ts:1213 only gates skills; agents still reports Synced N agents when no installed tool can receive them. AgentsHandler then resolves no destinations and writes nothing—the same phantom-success bug remains in the generic branch. The new test’s comment claims to cover agents, but no agent case is tested.
  • [P1 blocking] PR description — required pre-PR end-to-end coverage is incomplete. The trusted instructions require real-CLI verification across Claude, Codex, CodeBuddy, and OpenCode and providers git, gitlab, and github; the description records only a local tgit scenario and explicitly says npm run test:e2e was not run. Complete and document the required matrix before merge.

The gate only covered skills, so the generic branch still printed
`Synced N agents` when no installed tool could receive them — the same
phantom-success shape Tencent#585 describes, one resource type over.
AgentsHandler then resolves no destinations and writes nothing.

`hasInstalledTargetFor` also duplicated the walk that
`getInstalledResourceTargets` already performs. That function now takes
an optional `field`, and the gate calls it, so reporting and writing share
one resolver instead of two that can drift — an external HERMES_HOME or an
unresolvable workspace no longer disagrees between them.

Docs, rules and env never reach this branch; hooks and mcp have no
tool-path field to probe, so they keep reporting unconditionally.

Tests add the agent case the file's header already claimed: no tool
directory suppresses the claim and nothing lands, and the claim returns
once the directory exists. The suppression case is RED without the gate.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/pull.ts:1200 still duplicates the skills handler’s installation logic instead of using skillsDirForTool() or SkillsHandler.deliveryTargets(). An external HERMES_HOME can receive skills while the success line is suppressed, while an existing ~/.openclaw without a resolvable workspace can produce Synced N skills even though nothing is written.
  • [P1 blocking] src/pull.ts:1200 checks only whether any agents directory exists, not whether the selected agents can actually render to that tool. For example, with only Codex installed, an agent targeted exclusively at Claude—or a legacy agent unsupported by the installed tool—writes nothing but still produces Synced N agents; malformed agents similarly warn and are then counted as synced. Base the report/count on each item’s AgentsHandler.deliveryTargets().
  • [P1 blocking] PR description — required end-to-end coverage remains incomplete. The trusted instructions require real-CLI verification for Claude, Codex, CodeBuddy, and OpenCode and providers git, gitlab, and github; the description records only a local tgit scenario and explicitly states that npm run test:e2e was not run.

Resolved

  • The earlier finding that agents were not gated at all is resolved: the generic branch now includes agents, with absent/present-directory regression tests.

@jeff-r2026
jeff-r2026 merged commit d3637ea into Tencent:main Sep 23, 2026
11 checks passed
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.

2 participants