Skip to content

fix(pull): report actual sync outcome instead of team-repo item count - #597

Open
Morrowga wants to merge 2 commits into
Tencent:mainfrom
Morrowga:fix/pull-phantom-sync-count
Open

Morrowga wants to merge 2 commits into
Tencent:mainfrom
Morrowga:fix/pull-phantom-sync-count

Conversation

@Morrowga

Copy link
Copy Markdown

Fixes #574
Fixes #585

What this fixes

pull and init --agent both had the same underlying issue: the reported outcome didn't reflect what was actually written to disk.

pull (rules): pull.ts used the team-repo's rule count (scanTeamForPull's result) for the Synced N rule(s) message and totalSynced tally — regardless of whether any configured tool's rules/ directory actually existed. RulesHandler.pullItem() correctly skips writing to an uninstalled tool (by design, per isToolInstalled's own docstring), but that skip is only logged at debug level. So a user with no tool directories yet would see ✔ Synced 7 rule(s) with zero files actually written — exactly #574's repro.

init --agent: declaring --agent claude,codex never created those tool directories or ran an initial sync — it only printed "will auto-sync on each session start," relying entirely on a SessionStart hook that may not fire before the user's first real session.

--force: didn't bypass isToolInstalled anywhere, despite --force's own description ("sync regardless").

Changes

  • pull.ts: added hasInstalledRulesTarget() a single check per pull (not per-rule, since isToolInstalled doesn't vary across rules in the same batch) that gates whether the success message/count fires. When no tool is installed, warns with actionable next steps instead of claiming success.
  • rules.ts: pullAllRules() takes an optional force param; when true, pre-creates each configured tool's rules/ directory before the write loop, so isToolInstalled naturally passes and the existing write path proceeds — rather than special-casing a bypass inside pullItem (which isn't possible without changing the ResourceHandler abstract contract shared by every resource type).
  • known-agents.ts / init.ts / bootstrap.ts: renamed seedSelfModeToolDirsseedEnabledAgentDirs (per this issue's own suggestion) and reused it in both init's git and http paths. init --agent now seeds the declared tools' directories and runs a real pullForScope immediately.
  • pull.ts: exported pullForScope so init.ts can call it directly for the initial sync.

Scope

This covers rules specifically, matching this issue's repro and acceptance criteria. The same phantom-success pattern (team-repo count vs. actual-write count) exists in pull.ts for skills/docs/env/agents/team-culture/shared-instructions — same shape, different call sites. Flagging this as a known related gap rather than expanding this PR further; happy to follow up separately if useful.

Verification

  • npx tsc --noEmit clean
  • Full existing suite: 234 files / 3267 tests passing (one pre-existing flaky timeout in local-agent.test.ts's uninstall_teamai tests, confirmed via git stash to fail identically on unmodified main under full-suite load — unrelated to this change)
  • Added tests for both the rules-reporting fix and the init --agent sync, each confirmed to fail on unmodified code and pass with the fix (via git stash/stash pop)
  • Manually ran teamai init <repo> --agent claude end-to-end against a real throwaway GitHub repo and fresh directory — confirmed .claude/rules/*.md actually lands on disk with no manual mkdir, matching the acceptance criteria

Not covered

  • --force bypass is implemented for rules only, not the other resource types
  • Multi-provider verification (gitlab/tgit) — tested against GitHub specifically

pull's rules sync used the team-repo item count for its 'Synced N
rule(s)' message and totalSynced tally, regardless of whether any
configured tool's rules/ directory actually existed. When no tool was
installed, pullItem silently skipped every write (by design) but the
log still reported success with the full count.

- pull.ts: add hasInstalledRulesTarget(), a single per-pull check
  (isToolInstalled doesn't vary per rule) used to gate the success
  message and totalSynced increment; warn with actionable guidance
  when nothing was actually written.
- rules.ts: pullAllRules() now accepts an optional force parameter;
  when true, pre-creates each configured tool's rules/ directory
  before the write loop so --force means 'sync regardless', as its
  own description says, without touching pullItem's signature (part
  of the ResourceHandler abstract contract).
- known-agents.ts/init.ts/bootstrap.ts: renamed seedSelfModeToolDirs
  to seedEnabledAgentDirs and reused it from init's git and http
  paths. init --agent now seeds the declared tools' directories and
  runs one real pullForScope immediately, instead of leaving them
  empty until a SessionStart hook fires (which may never happen).
- pull.ts: exported pullForScope so init.ts can trigger the initial
  sync directly.

Fixes Tencent#574
Fixes Tencent#585
@SaulMoro

Copy link
Copy Markdown
Contributor

The hasInstalledRulesTarget gate is the right shape, and the test that fails on unmodified code is the part that makes this reviewable. One observation that could turn the "known related gap" in your Scope section into a few lines here, rather than a follow-up PR.

That per-tool installed-directory test already exists seven times in src/pull.ts. At cccbe1d, before this PR:

where line field it reads
skills sync 368 toolPath.skills
getInstalledResourceTargets 487 skills, rules, agents
tombstone cleanup 771 toolPathField, from the tombstoneTypes table at 753
skills cleanup 825 toolPath.skills
rules cleanup 992, 1023 toolPath.rules
agents cleanup 1266 toolPath.agents

Every one walks scopedToolPaths, reads a single toolPath field, calls ResourceHandler.isToolInstalled, and filters on isAgentExcluded. hasInstalledRulesTarget makes it eight.

If that helper takes the field as a parameter instead, something like hasInstalledTargetFor(teamConfig, localConfig, field), then the generic branch at src/pull.ts:740-745 can gate skills, docs and agents with no new code, and getInstalledResourceTargets reduces to the same helper over its three fields. The phantom count for the other resource types closes here instead of staying open.

Not blocking. Rules-only is a coherent scope if you would rather keep the diff tight for review, and the tests you added do not change either way.

Merge: resolved conflict in src/pull.ts — kept hasInstalledRulesTarget
(this PR's fix) alongside upstream's cleanupTombstonedResources
refactor (independent additions, no logic overlap).

Generalize: per review feedback on this PR, the 'walk tools, check
isToolInstalled, skip if excluded' pattern behind hasInstalledRulesTarget
already existed 7 times in pull.ts. Replaced it and getInstalledResourceTargets
with two shared helpers:
  - installedToolsFor(teamConfig, localConfig, field): tool ids with an
    installed <field> directory (rules/skills/agents)
  - hasInstalledTargetFor(...): boolean convenience wrapper

Applied the same phantom-success gate already built for rules to the
shared skills/agents write path, so 'no tool installed' now warns
instead of silently claiming success there too.

docs was checked and deliberately left out: DocsHandler.pullItem writes
unconditionally to a single fixed directory (fse.copy, which creates
the destination as needed) — there is no isToolInstalled gate or
per-tool skip to fix for docs.

Fixes Tencent#574
Fixes Tencent#585
@Morrowga

Copy link
Copy Markdown
Author

The hasInstalledRulesTarget gate is the right shape, and the test that fails on unmodified code is the part that makes this reviewable. One observation that could turn the "known related gap" in your Scope section into a few lines here, rather than a follow-up PR.

That per-tool installed-directory test already exists seven times in src/pull.ts. At cccbe1d, before this PR:

where line field it reads
skills sync 368 toolPath.skills
getInstalledResourceTargets 487 skills, rules, agents
tombstone cleanup 771 toolPathField, from the tombstoneTypes table at 753
skills cleanup 825 toolPath.skills
rules cleanup 992, 1023 toolPath.rules
agents cleanup 1266 toolPath.agents
Every one walks scopedToolPaths, reads a single toolPath field, calls ResourceHandler.isToolInstalled, and filters on isAgentExcluded. hasInstalledRulesTarget makes it eight.

If that helper takes the field as a parameter instead, something like hasInstalledTargetFor(teamConfig, localConfig, field), then the generic branch at src/pull.ts:740-745 can gate skills, docs and agents with no new code, and getInstalledResourceTargets reduces to the same helper over its three fields. The phantom count for the other resource types closes here instead of staying open.

Not blocking. Rules-only is a coherent scope if you would rather keep the diff tight for review, and the tests you added do not change either way.

Good catch generalized it. installedToolsFor(teamConfig, localConfig, field) now backs both the phantom-success gate and getInstalledResourceTargets, and I applied the same gate to the shared skills/agents write path.

One adjustment: left docs out. DocsHandler.pullItem writes unconditionally to a single fixed directory via fse.copy (which creates the destination as needed) there's no isToolInstalled check or per-tool skip there, so there's nothing to gate. Happy to be corrected if I'm missing something on that.

Merge conflict with #598's cleanupTombstonedResources refactor is resolved too kept both, no overlap.

@SaulMoro

Copy link
Copy Markdown
Contributor

installedToolsFor is the right shape, and you were right about docs — I was wrong to include it. DocsHandler.pullItem copies into one fixed localDocsDir with fse.copy, which creates the destination, so there is no per-tool skip to gate. Nothing to correct there.

One thing from reading the new commit, not blocking.

The gate is a boolean over tools, and the write path is per tool. hasInstalledTargetFor (src/pull.ts:499) reduces installedToolsFor to .length > 0, while SkillsHandler.pullItem skips each uninstalled tool on its own (src/resources/skills.ts:505, and src/resources/agents.ts:421 for agents). So with enabledAgents: [claude, codex], Claude's directory present and Codex's absent, the gate passes and Synced 12 skills prints while Codex receives nothing — #574's report, one level down.

The list is already in hand, since installedToolsFor returns the tools and only the caller discards them:

-const hasTarget = await hasInstalledTargetFor(freshConfig, localConfig, type as 'skills' | 'agents');
-if (!hasTarget) {
+const installed = await installedToolsFor(freshConfig, localConfig, type as 'skills' | 'agents');
+if (installed.length === 0) {
   log.warn(`… no installed tool directory found — nothing written…`);
 } else {
   …
-  log.success(`[${scopeLabel}] Synced ${items.length} ${type}`);
+  log.success(`[${scopeLabel}] Synced ${items.length} ${type} → ${installed.join(', ')}`);

That turns the count into a claim a reader can check, and it costs a variable. Whether a partially missing tool should warn as well as be omitted from that list is a design call I would leave to you — naming the tools that were written is already most of the value for someone reading their terminal.

Unrelated, in case the file list raises an eyebrow: #599 (phase 1 of #598) rewrites src/doctor.ts, which also appears here. The two do not collide — your branch's copy of that file is main's, carried in with the upstream merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants