Skip to content

feat(doctor): run the checks after a pull, and check what actually landed - #625

Open
SaulMoro wants to merge 10 commits into
Tencent:mainfrom
SaulMoro:feat/pull-checks-delivery-598
Open

SaulMoro wants to merge 10 commits into
Tencent:mainfrom
SaulMoro:feat/pull-checks-delivery-598

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Phases 2 and 3 of #598. An interactive teamai pull now ends by running the doctor checks and printing what failed, and the registry gained the checks that look at the payload rather than the plumbing: the skills and docs a pull reported syncing are verified to be on disk and readable.

 teamai pull
   sync()
   report("Synced N skills")
+  buildChecks() → print each failure with its fix     (interactive only)

 hook session-start
   pull({ silent: true })                              unchanged, runs no checks

 buildChecks()
+  + <tool> is installed          enabledAgents names it, nothing is here
+  + Skills delivered to <tool>   per tool, per skill, SKILL.md readable
+  + Team docs delivered          the bundle against sharing.docs.localDir

The delivery check separates three outcomes, because they need different fixes:

flowchart LR
  D["desired set<br>role namespaces ∪ subscribed tags − excluded"] --> R{"per enabled tool:<br>resolve destination, stat, read SKILL.md"}
  R -->|"directory exists, frontmatter name matches"| OK["ok"]
  R -->|"no directory"| MISS["not delivered<br>→ teamai pull"]
  R -->|"frontmatter broken or name mismatched"| BAD["delivered but unreadable<br>→ fix SKILL.md in the team repo"]
Loading

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Refactor / internal cleanup

Decisions worth a reviewer's attention

Post-pull runs the local checks only. Check gained source: 'local' | 'provider', and the post-pull pass filters out the provider ones. The pull just used the provider successfully, so re-probing gh auth status would add a subprocess to every sync and prove nothing new. teamai doctor still runs the full registry — verified below on gitlab and github.

The "enabled but not installed" check is not the one-liner the issue described. #598 proposed turning the continue in buildHookChecks into a failing check. Driving the real CLI showed that never fires for OpenCode or CodeBuddy: they ship skills and no hook configuration, so the function returns before the question is asked — exactly the tools most likely to be declared and absent. It is now its own builder over ctx.toolPaths, probing a resource path (resources land under resolveToolBaseDir, the project root in project scope).

One resolver per destination, shared with the write path. skillTargetForTool (src/resources/skills.ts) and resolveDocsDestination (src/resources/docs.ts) are extracted from pullItem and used by both. A second copy of those gates is how "Synced 12 skills" ends up true for one tool and silently false for another.

docs is included, rules/agents/mcp are not. Docs are one fixed directory, so the check is a tree compare and costs nothing extra. Rules need a per-tool destination resolver (.mdc with derived frontmatter, Copilot instructions), and agents carry spec.targets per item plus three render formats — so their desired set is a relation, not a product, and checking them means parsing every agent YAML on every pull. That deserves its own design and its own review: #624.

Known limits, stated rather than hidden. A delivery check's check() closes over a scan done when the registry was built, so the fix can name the missing items; it is consistent within a run, not re-entrant. And per-tool granularity for an uninstalled tool depends on enabledAgents being set: without it, the team's tool list is aspirational and an absent tool stays silent, as it always has.

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run passes — 247 files, 3387 tests
  • npm run test:e2e passes — 33 files, 153 tests (3 files / 26 tests skipped, network-gated)
  • Added tests: pull-post-checks.test.ts (6), doctor-delivery.test.ts (16), desired-skills.test.ts (6), plus 4 cases in doctor.test.ts

Real CLI end-to-end

Throwaway HOME, local git remote, project scope, four agents declared (claude, codex, codebuddy, opencode), run against providers git, gitlab and github.

A hand-deleted skill is reported, with the tool and the name

$ rm -rf project/.claude/skills/beta && teamai doctor --json
    FAIL: Skills delivered to claude
      fix: In claude, not delivered: beta. Run `teamai pull --force`: a plain pull skips
           a scope whose team repo has not changed, so it cannot restore this. ...
    ok: false

A skill that landed but is invisible is reported differently, at the end of the pull

$ printf -- "---\nname: not-beta\n---\n" > project/.claude/skills/beta/SKILL.md
$ teamai pull
  ✔ [project] Already synced at 8adce48, skipping
  ⚠ Pull finished, but 1 check(s) failed:
  ⚠   ✖ Skills delivered to claude
      → In claude, delivered but unreadable: beta. Run `teamai pull --force`: a plain pull
        skips a scope whose team repo has not changed, so it cannot restore this. If a
        skill stays unreadable, fix its SKILL.md in the team repo — the frontmatter needs
        a `name` matching the directory, or the agent never discovers it.

An enabled tool that is not installed

$ teamai pull        # enabledAgents: [claude, opencode], no .opencode here
  ✔ [project] Synced 2 skills (all updated)
  ⚠ Pull finished, but 1 check(s) failed:
  ⚠   ✖ opencode is installed
      → enabledAgents lists opencode, but it has no directory under <project>, so a pull
        delivers nothing to it. Install opencode (in project scope, opening a session
        there creates its root), or run `teamai uninstall --agent opencode` to stop
        syncing to it.

The docs bundle

$ rm -f project/team-docs/api/reference.md && teamai pull
  ⚠   ✖ Team docs delivered
      → Missing from <project>/team-docs: api/reference.md. Run `teamai pull --force`: a
        plain pull skips a scope whose team repo has not changed, so it cannot restore
        these.

Why --force and not teamai pull. That pull printed Already synced, skipping and did not restore the deleted file: the revision fast-path skips a scope whose team repo has not moved, so a plain pull cannot heal drift — which is the main case these checks exist to catch. The first version of these fixes said "Run teamai pull", printed at the end of a pull; it was both useless and wrong. The underlying gap — that a plain pull does not notice a resource has gone missing — deserves its own issue and its own decision (widening the lastPullTargets fingerprint costs a listDirs per tool on the SessionStart path); healing is out of scope here.

All four agents, once their roots exist

.claude:    alpha beta      .codebuddy: alpha beta
.codex:     alpha beta      .opencode:  alpha beta
  ok: true
    PASS Skills delivered to claude / codex / codebuddy / opencode
    PASS Team docs delivered

The hook path stays silent, --dry-run runs nothing, and the provider is never probed

$ teamai pull --silent          → no output at all
$ teamai pull --dry-run         → dry-run lines only, no checks

provider: gitlab   pull: silent about auth   doctor: FAIL GitLab token is configured
provider: github   pull: silent about auth   doctor: FAIL gh CLI is authenticated

Related Issues

For #598 (phases 2 and 3; phase 1 landed in #599). Follow-up for the remaining primitives: #624.

Notes for Reviewers

This is based on origin/main, not on #597. That PR is @Morrowga's and is currently conflicting with main, so I did not block on it — as discussed in the issue. It touches src/pull.ts and src/doctor.ts too, so a conflict is expected and it is mine to resolve: #597 keeps priority and I rebase this branch after it merges.

Docs are updated in both guides and both languages (docs/usage-guide.md, docs/usage-guide.zh-CN.md) plus a CHANGELOG entry. No README change: the teamai doctor row still describes the command, and touching it would cost five synchronized translations.

Every line a pull prints reports what it did; none reported what is on
disk. That gap is the shape of Tencent#574, Tencent#525, Tencent#342 and friends: "Synced N
skills" and the tool receives nothing.

An explicit `teamai pull` now re-runs the doctor registry and prints only
the checks that failed, with the fix each one already carries. The
SessionStart hook path (`pull({ silent: true })`) and `--dry-run` run no
checks at all, so session startup is unchanged.

Checks now declare `source: 'local' | 'provider'`. The post-pull pass runs
the local ones only: the pull just used the provider successfully, so
re-probing `gh auth status` would add a subprocess to every sync and prove
nothing new. `teamai doctor` still runs the full registry.

For Tencent#598.
buildHookChecks skipped any tool whose settings directory was missing —
the same silent skip Tencent#574 reports in pull, reproduced inside doctor. With
`claude` installed and `codex` not, the report was all green while codex
received nothing.

A tool listed in `enabledAgents` is the user's own claim that they use it,
so it now yields a failing `<tool> is installed` check with a fix that
points at `teamai uninstall --agent <tool>`. Without `enabledAgents` the
team's tool list is aspirational and an absent tool stays silent, so no
existing install grows a new red line.

For Tencent#598.
pullForScope computed the desired skill set — role namespaces union the
subscribed tags, minus the exclusions — and dropped it when the run ended.
The delivery check needs the same set, and re-deriving it there would put
that policy in a second place that drifts on its own.

The block moves to an exported, read-only resolveDesiredSkills(), called
from where it stood. roleContext stays an explicit argument: pullForScope
already holds one, and null means "no roles configured", not "not looked
up yet". No behaviour change.

For Tencent#598.
Every other check verifies plumbing — provider CLI, clone, config, hooks,
env. None verified the payload, which is what Tencent#574, Tencent#525, Tencent#342 and Tencent#372
are actually about: the run reports success and the agent finds nothing.

`Skills delivered to <tool>` compares the desired set (role namespaces
union subscribed tags, minus exclusions) against what is on disk for each
installed tool, and names the skills that are missing. It catches what a
write-time gate cannot: per-tool skips, and drift after a correct pull —
a directory deleted by hand, a tool reinstalled, a role changed.

Destination resolution moves into skillTargetForTool(), so pull writes and
doctor checks the same paths, including Codex's shared .agents/skills
directory. A tool that is not installed is asked for nothing; enabledAgents
covers that case with its own check.

For Tencent#598.
A copy can arrive intact and still never be discovered: SKILL.md deleted,
frontmatter that does not parse, or a `name` that does not match its
directory (Tencent#372's class). The write succeeded, so no write-time gate has
anything to report.

The delivery check now separates the two causes — "not delivered" from
"delivered but unreadable" — and the fix says which one `teamai pull` can
repair and which one needs the team repo fixed.

For Tencent#598.
Hanging "is this tool installed" off the hook registry made it invisible
for exactly the tools most likely to be declared and absent: OpenCode and
CodeBuddy ship skills and no hook configuration, so `buildHookChecks`
returned before the question was ever asked. Found driving the real CLI:
`enabledAgents: [claude, opencode]` with no OpenCode root printed nothing.

The check moves to its own builder over ctx.toolPaths, and probes a
resource path rather than the settings path — resources land under
resolveToolBaseDir (the project root in project scope), which is the root
a pull would have to write into.

For Tencent#598.
Two findings from the review pass.

A repo with the same skill in two active namespaces makes
scanRoleAwareSkills throw. pullForScope catches it and the post-pull pass
catches it, but `teamai doctor` called buildChecks unguarded: the command
whose job is explaining bad state stack-traced on it. It now reports a
failing "Skills to deliver can be resolved" check carrying the collision.

`copilot is installed` could never fail — isToolInstalledForConfig counts
Copilot as installed as soon as enabledAgents names it — so that dead check
is gone. Copilot's delivery check still reports what did not arrive.

Also: pull and doctor now share formatCheckResult instead of two copies of
the same glyphs, the timeout message interpolates its constant, and the
DesiredSkills block no longer sits between skillSafeToRemove's doc comment
and its function.

For Tencent#598.
Both guides, both languages, same positions: the manual-pull block, the
doctor section, the exclusion and tag-subscription paragraphs, and the
packages-section one-liner that enumerated what doctor checks.

No README change: the `teamai doctor` row still describes it, and touching
it would cost five synchronized translations.

For Tencent#598.
Docs are the one payload with a single destination instead of one per
tool, so the check is a tree comparison rather than a per-tool loop:
every non-dot file under the team repo's `docs/` against
`sharing.docs.localDir`, with the same filter the copy uses.

The destination resolution moves out of DocsHandler.pullItem into
resolveDocsDestination(), so pull writes and doctor checks the same
directory — including the project-scope rule that a `~/` prefix means the
project root, not HOME.

Found while validating it: a doc deleted by hand is not restored by the
next pull, because the rev fast-path skips the scope. The check is what
makes that visible.

For Tencent#598.
The delivery fixes said "Run `teamai pull`" — printed at the end of a
`teamai pull`, and wrong besides: a scope whose team repo has not moved is
skipped by the revision fast-path, so a plain pull cannot restore a
resource deleted after a correct sync, which is the main case these checks
exist to catch.

Both fixes now say `teamai pull --force` and why. The underlying gap —
that a plain pull does not heal drift — is filed as its own issue.

For Tencent#598.
@jeff-r2026 jeff-r2026 self-assigned this Sep 18, 2026
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