feat(doctor): export the check registry and add --json - #599
Merged
Merged
Conversation
The checks that catch "reported success, nothing on disk" lived inside doctor() as a local array, so nothing else could run them, and the only machine-readable result was the exit code Tencent#569 added. Extract resolveDoctorContext() and buildChecks(), which render nothing, and add --json: one object on stdout, every log line on stderr, exit code unchanged. Human output is byte-for-byte what it was.
This was referenced Sep 16, 2026
jeff-r2026
approved these changes
Sep 17, 2026
jeff-r2026
left a comment
Collaborator
There was a problem hiding this comment.
LGTM — approving. Clean phase-1 extraction: human output is behavior-equivalent (13 untouched tests prove it) and --json keeps stdout pure — checked every console.log is !jsonMode-guarded (setStderrOnly only redirects log.*), and resolveHookScope path is preserved.
4 tasks
jeff-r2026
added a commit
that referenced
this pull request
Sep 17, 2026
…606) src/doctor.ts imported LocalConfig twice — once in the top type-only import (line 5) and once in the grouped import block from './types.js'. TypeScript rejected this with TS2300 (Duplicate identifier 'LocalConfig'), so `tsc --noEmit` failed and the main CI has been red since #599. The two imports landed cleanly as a merge (no textual conflict) but collided at the type level, so each PR's own branch build was green. Drop the LocalConfig binding from line 5 and keep it in the grouped block alongside TeamaiConfig, matching the surrounding style.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
teamai doctorholds the only checks that catch this repo's most common defect shape — the command reports success and nothing lands on disk (#574, #525, #342, #335, #331, #508, #436, #585). #569 made that result actionable for a human:doctor()returnsallPassedand the command exits 1. Two things still stopped anything else from using it. The registry was a local array insidedoctor(), so no other caller could run it, and there was no--json, so a hook, a CI job or an agent could read the exit code but never which check failed or how to fix it.This is phase 1 of #598: make the registry callable and the result parseable. Nothing else changes — same checks, same human output, same exit codes.
The two modes differ only in where the bytes go:
--jsonemits the{ name, ok, fix }shape the checks already carried, so the flag adds a channel rather than a second source of truth:{ "ok": false, "scope": "user", "checks": [ { "name": "Team repo exists locally", "ok": true }, { "name": "teamai hooks in claude settings", "ok": false, "fix": "Run `teamai hooks inject` to inject/update hooks" } ] }packagesappears only when the team repo declares packages, and carriespkgDoctorReport's rendered lines as what they are — human text, not checks.notesappears only when there is an advisory; today that is the Codex trust-gate reminder, which would otherwise be lost in JSON mode.This does not touch
src/pull.ts, so it does not collide with #597.Type of Change
Test Plan
Branched from
origin/main@c674ffe.npx tsc --noEmitpassesnpx vitest runpasses (233 files, 3278 tests)Unit,
src/__tests__/doctor.test.ts— the 13 existing tests are untouched and still green, which is what proves the extraction did not move the human rendering. Five added:The
buildCheckstest assertsconsoleSpywas never called: building the registry has to render nothing, or phases 2 and 3 cannot reuse it. The JSON helper insists on exactly oneconsole.log, so every JSON test also proves stdout purity rather than tolerating a leak.E2E,
src/__tests__/e2e/doctor-cli.test.ts, spawning the built CLI with a sandboxedHOME— 5 tests, two added, both parsingresult.stdoutwhole:End-to-end by hand against
dist/index.js, with throwawayHOMEs.--json 2>/dev/nullhad to parse whole in every case, since that is the contract a CI job depends on:--jsonstdoutok: true, 4 checks, 373 bytes, parsesok: false, failing check carries itsfix, 440 bytes, parsesok: false,scope: null, one check, 224 bytes, parsesAgent matrix, since
toolPathsdrives one hook check per tool:Provider matrix, since the provider branch decides which checks exist:
Output is English only; the sole non-ASCII characters are the
✔ ✖ → ℹ ⚠glyphs already in use, and the JSON is pure ASCII.Related Issues
Part of #598 — phase 1 of three. It does not close the issue: phase 2 (running the checks at the end of an interactive pull) and phase 3 (checking that the skills
pullreported actually reached the tool directories) still stand.Notes for Reviewers
What this unlocks is the point, not the flag. Phase 2 needs a registry it can build without rendering; phase 3 needs somewhere to put a delivery check. Both are now one call, and neither has to re-derive the
toolPathsnarrowing (#569) or theresolveHookScopebaseDir (#264).The issue names the export
buildChecks(config, scope); this usesbuildChecks(ctx). A context object is the honest version: the registry also needstoolPathsandbaseDir, and a(config, scope)signature would force every caller to re-derive both — the second place that drifts on its own, which #598 rejects elsewhere. I will correct the issue text to match, includingresolveDoctorContext, which is now part of the phase-2 contract.Three things deliberately not done.
pkgDoctorReportstill returns pre-rendered lines, so JSON carries them underpackagesinstead of being refactored into checks. Promoting it would dragsrc/pkg/and its tests into a PR aboutdoctor. It is the obvious follow-up if a consumer needs those structured.runChecksand the loop stay private. Phase 2 will want them, but exporting for a caller that does not exist yet is generality on credit;CheckResultandDoctorReportare exported, because those are the wire contract.Check.checkkeeps its name, although a thunk named as a noun reads oddly now that the interface is public. Renaming it toruntouches all eleven registry entries, and their diff currently reads "moved verbatim", which is what makes the extraction reviewable. Happy to do it in a follow-up.The JSON keys off display names. Rewording a check's
nameis a breaking change for a script that greps it. A stableidper check would fix that and can land later without breaking{ name, ok, fix }; it seemed premature before anyone consumes this.setStderrOnly(true)is a process global that is never reset, matchinghook-dispatch-cli.ts:187andcontribute-check.ts:702. It is reachable only throughdoctor(), and phase 2 will callbuildChecksdirectly, so no in-process caller inherits it. Worth knowing before someone callsdoctor({ json: true })from inside another command.The
ja/ko/thREADME rows are mine and I do not speak those languages. They mirror the structure of the neighbouring row and only add the flag, but a native reader should confirm the phrasing. Dropping them and keeping onlyREADME.md/README.zh-CN.mdwould leave three of five tables stale, which seemed worse.