Skip to content

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

Merged
jeff-r2026 merged 15 commits into
Tencent:mainfrom
SaulMoro:feat/pull-checks-delivery-598
Sep 18, 2026
Merged

jeff-r2026 merged 15 commits into
Tencent:mainfrom
SaulMoro:feat/pull-checks-delivery-598

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

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)
+                  minus provider checks, and minus what the pull already said

 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.

Rebased onto origin/main, and one collision resolved. #621 landed after this branch was opened and put a Contributed learnings are published check on the registry this PR now re-runs. Keeping both as-is made one teamai pull say this:

⚠ 1 learning(s) are written locally but not published: <push error>.
  They stay recallable here; run `teamai doctor` for what to check.
…
⚠ Pull finished, but 1 check(s) failed:
⚠   ✖ Contributed learnings are published
    → Run `teamai pull` to publish them.        ← the pull that just ran

Two lines for one fact, pointing at each other. Check therefore gained reportedByPull, and the post-pull pass skips a check whose topic this run reported.

The warning is the one that stays, because it carries lastError from publishQueuedLearnings — the push error. doctor cannot learn that without attempting a push of its own, which a read-only diagnostic must not do. And #621's fix is left untouched, because in teamai doctor it is correct: the queue publish runs before the revision fast-path, so unlike the delivery checks, a plain teamai pull really is the retry there.

Evidence, not a declaration. pullForScope returns before the publish step when the team repo fails to refresh, and swallows a publish throw into a debug line. On both paths the pull says nothing about the queue, so suppressing the check unconditionally would leave a stuck queue reported by nobody. pull() records the topics it actually reported and the filter reads that; the topic is a union rather than a boolean so a second tagged check cannot be silenced by the first one's evidence. Both paths are covered by tests and by a real-CLI run below.

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. Phase 3 asks for a check that looks at the payload rather than the plumbing. Docs are payload delivered by the same pull, through the same kind of destination resolver this PR extracts, so leaving them out would ship the idea half done while the resolver sits right there. The line is drawn at what a desired set costs to state: docs are one fixed directory, and so are skills once resolveDesiredSkills exists. 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 rather than a product and checking them means parsing every agent YAML on every pull. That is a different design question, and it has its own issue: #624. Reviewers who read phase 3 more narrowly should say so; Team docs delivered is one commit and comes out cleanly.

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.

Four fixes from a review pass, after the rebase. resolveSkillDestination warned about a Codex shared-directory conflict whenever it was called without a team copy to compare against, which is exactly what the read-only callers do: teamai remove skills already did on main, and this branch added buildDeliveryChecks, so a plain teamai doctor warned once per skill about copies the pull treats as identical. Omitting sourcePath now returns the shared destination before the reconciliation branch, which is what that function's docstring already promised.

The post-pull evidence moved out of module scope into pull(), which passes it down as a required parameter of pullForScope; PullReportedTopic is gone and Check.reportedByPull is a plain string. The 5s budget now covers buildChecks as well as runChecks, because the delivery checks do their I/O while the registry is built, and the pass prints one line instead of going quiet when it gives up. And <tool> is installed reports an installed tool as passing rather than omitting it, so doctor --json carries an entry per enabled tool either way.

Test Plan

  • npx tsc --noEmit passes
  • npm run build passes
  • npx vitest run passes — 248 files, 3411 tests
  • npm run test:e2e passes — 34 files, 154 tests (3 files / 26 tests skipped, network-gated)
  • Added tests: pull-post-checks.test.ts (10), doctor-delivery.test.ts (18), desired-skills.test.ts (6), plus 6 cases in doctor.test.ts
  • Every commit typechecks in isolation: the commit that makes source required now sets it on feat(contribute): make the pending queue the write path, and surface it #621's check too, so the commits between it and the merge resolution are not left with a Check literal missing a required field.

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.

A stuck queue is reported once, by the pull, with the reason — and not repeated as a check

$ teamai pull --force
  ✔ [user] Team repo: already up to date
  ⚠ 1 learning(s) are written locally but not published: … [remote rejected]
    (unpacker error) … . They stay recallable here; run `teamai doctor` for what to check.
  ✔ [user] Synced 2 skills (all updated)

No Pull finished, but N check(s) failed block. teamai doctor still carries it, and doctor --json is unchanged — neither source nor reportedByPull is serialized, the report is still { name, ok, fix }.

… but a pull that never reached the publish step does print it

$ mv origin.git elsewhere && teamai pull --force
  ✖ [user] Pull failed: fatal: '…/origin.git' does not appear to be a git repository
  ⚠ Pull finished, but 1 check(s) failed:
  ⚠   ✖ Contributed learnings are published
      → Run `teamai pull` to publish them. If they stay queued, check that you
        can push to the team repo (run with --verbose to see the push error).

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 rebased on origin/main (currently #631). The one conflict was in buildChecks's registry array, where #621 and this branch inserted at the same point; both sides are kept, and the resulting double-report is the reportedByPull decision above.

Still based on origin/main rather than #597. That PR is @Morrowga's and is 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 #597 keeps priority and I rebase this branch again 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.
… said

Tencent#621 landed a "Contributed learnings are published" check on the same
registry this pass now runs. A pull with a stuck queue therefore said the
same thing twice, and contradicted itself doing it: pullForScope warns
"run `teamai doctor` for what to check", then the post-pull block answers
with the check's own fix, "Run `teamai pull` to publish them" — the pull
that had just run.

The warning is the better of the two and has to stay: it carries the push
error, which the check cannot learn without attempting a push of its own,
and `doctor` is a read-only diagnostic. Rewording the fix is no good
either, because in `teamai doctor` — where the queue publish runs before
the revision fast-path, so a plain pull really is the retry — that advice
is correct.

So `Check` gains `reportedByPull`, and the post-pull pass skips a check
whose topic this run reported. Evidence, not a declaration: pullForScope
returns before the publish step when the team repo fails to refresh, and
swallows a publish throw into a debug line. On both paths the pull says
nothing about the queue, so suppressing the check unconditionally would
leave a stuck queue reported by nobody.

The topic is a union rather than a boolean for the same reason: the pull
proves what it reported by naming it, so a second tagged check cannot be
silenced by the first one's evidence.

For Tencent#598.
…y does

`nameList` was written for the docs check and used only there, while the
delivery check — the one most likely to have a long list, since a fresh
machine is missing every skill at once — joined its names unbounded. A
member with forty desired skills got all forty pasted into one fix line.

Both now go through the helper, which moves above its first caller.

Also drops a stray blank line that a rebase left between
`skillSafeToRemove`'s docstring and the function, detaching the two.

For Tencent#598.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Rebased onto origin/main (currently #631). One conflict, in the buildChecks registry array, where #621 and this branch both inserted after the team config check. Both sides are kept.

#621 put a Contributed learnings are published check on the registry this PR re-runs at the end of a pull, so a pull with a stuck queue said the same thing twice and contradicted itself. pullForScope warns "run teamai doctor for what to check", then the post-pull block answered with that check's own fix, "Run teamai pull to publish them". Two commits on top:

  • Check gained reportedByPull, and the post-pull pass skips a check whose topic that run already reported. It reads what the pull actually printed rather than trusting the flag, because pullForScope returns before the publish step when the team repo fails to refresh, and swallows a publish throw into a debug line. Suppressing unconditionally would leave a stuck queue reported by nobody.
  • The delivery check now caps its name list through nameList, which only the docs check used before. A fresh machine is missing every skill at once, so that fix line could otherwise print the whole desired set.

I also amended the commit that makes source required so it sets it on #621's check, which keeps every commit typechecking on its own.

Description updated. Full test plan re-run: 248 test files and 3407 tests, e2e 34 files and 154 tests, plus the real CLI against the git, gitlab and github providers and a four agent matrix. The stuck queue path and the abort before publish path are both covered there.

CI is green apart from Codex PR Review, which exits before it installs anything: "Actor 'SaulMoro' is not permitted to run this action: must have write access to Tencent/teamai-cli. Detected permission: 'read'." That is the workflow's access check, not this branch.

Reviewing the docs delivery check also turned up #635, where sharing.docs.localDir resolves against the process cwd. It predates this PR and is filed separately.

Ready for review.

@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 18, 2026
`resolveSkillDestination` warns when a skill exists in both `.agents/skills`
and `.codex/skills`, unless it can prove the two are identical. That proof
needs the team copy, so the check is written as `sourcePath && ...` — and
without a sourcePath the guard short-circuits into the warning instead of
past it.

The read-only callers are the ones that omit it. `teamai remove skills`
already did on main; this branch added `buildDeliveryChecks`, so the warning
now fires once per skill on every `teamai doctor` and at the end of every
pull, for copies the write path silently reconciles.

Omitting sourcePath now returns the shared destination before the
reconciliation branch, which is what the function's own docstring already
promised. The write path is untouched: with a source, an unprovable pair
still warns.

For Tencent#598.
… both ways

Three things a review of the post-pull pass turned up.

The set of what a run already said was a module-level `const` cleared at the
top of `pull()`, and the topic it held was a union with one member: two pieces
of machinery where one value does. `pull()` now owns the set and passes it
down. It is a required parameter of `pullForScope` rather than a field on its
optional `policy`, because a call site that forgot it would stop recording
silently, which is the failure the mechanism exists to prevent.
`PullReportedTopic` is gone and `Check.reportedByPull` is a plain string.

The 5s budget wrapped `runChecks` only, while the I/O is in `buildChecks`:
the delivery checks stat every desired skill for every tool as the registry is
built. Both are inside it now. And the pass no longer goes quiet when it gives
up — silence after spending the whole budget is the same "reported success,
nothing happened" shape these checks exist to catch, so it says one line and
points at `teamai doctor`. The reason stays on the debug channel.

`<tool> is installed` only pushed a check when it already failed, so an
installed tool had no entry at all. `doctor --json` is consumed by hooks and
CI, where a missing entry cannot be told apart from one that passed, and no
other check in the registry behaves that way. It now reports both ways.

Docs in both languages and the CHANGELOG follow, including a note that the
checks at the end of a pull cover the scope resolved from the current
directory.

For Tencent#598.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Four fixes from a review pass. The first one is the reason for this push.

resolveSkillDestination warns when a skill sits in both .agents/skills and .codex/skills unless it can prove the two are identical. That proof needs the team copy, so the condition reads sourcePath && ..., and without a sourcePath the guard falls into the warning instead of past it. The callers that omit it are the read-only ones. teamai remove skills already did on main; this branch added buildDeliveryChecks, so a plain teamai doctor warned once per skill:

$ teamai doctor          # byte-identical copies in both directories
⚠ Codex skill conflict for alpha: keeping different copies in .agents/skills and .codex/skills
  ✔ Skills delivered to codex

Omitting sourcePath now returns the shared destination before the reconciliation branch, which is what that function's docstring already promised. The write path is untouched, so a pair it cannot prove identical still warns.

The other three:

  • The post-pull evidence lived in a module-level Set and a one-member union type. pull() owns the set now and passes it down as a required parameter of pullForScope, so a call site cannot forget it and stop recording silently. PullReportedTopic is gone and Check.reportedByPull is a plain string.
  • The 5s budget wrapped runChecks only, while the I/O is in buildChecks: the delivery checks stat every desired skill for every tool as the registry is built. Both are inside it now, and the pass prints one line instead of going quiet when it gives up.
  • <tool> is installed only appeared when it failed, so doctor --json had no entry for an installed tool and a consumer could not tell that apart from one never evaluated. It reports both ways now.

Verified again end to end: 248 test files and 3411 tests, e2e 34 files and 154 tests, and the real CLI against the git, gitlab and github providers plus the four agent matrix, which now shows a line per enabled tool. The stuck queue path and the abort before publish path both still behave as described above.

I kept Team docs delivered in this PR and rewrote its justification in the description, which argued cost where it should have argued scope. If you read phase 3 more narrowly, it is one commit and comes out cleanly.

Codex PR Review still fails on the actor permission check, before it installs anything, so this branch has not been through it.

…clone

Three findings from the Codex review.

A pull that found a scope's lock held by another process drops that scope from
every stage that reads the shared clone, because the other process may have it
on a transient branch. The post-pull checks resolve their own context from that
same clone and ran anyway, so a diagnostic could report a failure about someone
else's work in progress. They now stay out entirely when any scope was
contended; `teamai doctor` runs them once the other process is done.

`<tool> is installed` probed the tool root while skill delivery asks
`skillTargetForTool`, which sends OpenClaw to its workspace directory, Hermes
to its home, and Copilot through `enabledAgents`. A `~/.openclaw` with no
workspace therefore passed the check while delivery skipped the tool and its
delivery check vanished — "reported success, received nothing" inside the
command written to catch it. The probe is now `skillsReachTool`, which asks
that same resolver; a tool that configures no skills path keeps the generic
one, having no such resolver to ask.

`Team docs delivered` called `pathExists`, which follows symlinks and says yes
to a directory, so a name occupied by something other than the document passed
while the document was no more readable than a missing one. It now requires a
file. Reading each one would cost more than the job needs on a bundle of
hundreds of documents, so this stats rather than reads, and the guides no
longer claim the docs check does everything the skills check does.

For Tencent#598.
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

All three taken. Two as written, one with a smaller remedy.

P1, post-pull diagnostics on a contended clone. Correct. contended drops a scope from every stage that reads the shared clone, with a comment at src/pull.ts:1681 saying why, and the checks resolve their own context from that same clone. The reachable case is ordinary: a manual teamai pull while a SessionStart hook holds the lock. The pass now returns early when any scope was contended, so the pull says the scope was skipped and stops there.

P2, OpenClaw. Correct, and the gap is wider than the tool root. resolveOpenclawWorkspaceDir wants ~/.openclaw/workspace, not ~/.openclaw, so a tool root on its own passed the check while skillTargetForTool returned null and the delivery check disappeared with it. Rather than special-case OpenClaw in doctor, the probe now goes through the resolver the sync itself uses:

export async function skillsReachTool(tool, configuredSkillsPath, localConfig): Promise<boolean> {
  return await skillTargetForTool(tool, configuredSkillsPath, localConfig, INSTALL_PROBE_SKILL) !== null;
}

That covers Hermes and Copilot by construction too, which a targeted fix would have left to drift. A tool that configures no skills path has no such resolver to ask and keeps the generic probe.

P3, docs readability. The gap is real and the wording was ours: the guide said the docs check "does the same" as the skills one, which distinguishes delivered from unreadable, while the code only called pathExists. A directory sitting on the name passed.

I did not go as far as reading each file. A bundle can hold hundreds of documents and the pass now runs inside a 5s budget on every pull, so reading them all is more than the job needs. stat plus isFile costs what pathExists cost, catches the directory and the dangling link, and leaves only permission-denied uncaught. The guides now say what the check does instead of claiming parity. Say the word if you want the full read.

Each fix has a test that fails without it: the OpenClaw one passes a tool root with no workspace, which the old probe accepted.

Verified again: 248 test files and 3415 tests, e2e 34 files and 154 tests, and the real CLI across the git, gitlab and github providers plus the four agent matrix.

On the Chinese-comment question in your notes: no new Chinese reaches production code. The repo rule is English in src/, with Chinese only in CLAUDE.md, AGENTS.md and the *.zh-CN.md guides, and this push follows that.

@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 18, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P2] “Readable” docs are only checked with stat(). isReadableFile() returns true for any regular file, even when the current user cannot open/read it. This can make Team docs delivered pass although the agent cannot consume the document. Attempt an actual read/open or use fs.access(..., R_OK). src/doctor.ts:211
  • [P2] Copilot violates the documented per-enabled-tool result. The code unconditionally skips Copilot, so doctor --json has no copilot is installed entry even when enabledAgents includes it. This contradicts the guide and PR claim that every enabled tool gets a passing or failing entry. Emit a passing check using the existing Copilot installation semantics, or narrow the documentation. src/doctor.ts:111

Testing

  • The PR description includes a test plan and concrete real-CLI end-to-end records across the required agents/providers; no blocking testing-description issue.
  • Per instruction, I only inspected the diff and did not run or build PR code.

@jeff-r2026
jeff-r2026 merged commit 701e472 into Tencent:main Sep 18, 2026
11 of 12 checks passed
@SaulMoro
SaulMoro deleted the feat/pull-checks-delivery-598 branch September 19, 2026 18:29
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