Skip to content

feat(settings): codeman doctor in Settings → System → Diagnostics - #536

Merged
Ark0N merged 5 commits into
Ark0N:masterfrom
opticon454:feat/doctor-in-settings
Oct 6, 2026
Merged

Ark0N merged 5 commits into
Ark0N:masterfrom
opticon454:feat/doctor-in-settings

Conversation

@opticon454

Copy link
Copy Markdown
Contributor

What

Surfaces codeman doctor in the web UI: Settings → System → Diagnostics → Run checks lists which agent CLIs, tmux, Node and the optional office tools are installed on the server, with versions, paths, required/optional, and the install hint for what is missing. Backed by GET /api/doctor[?category=core|office|other].

Design

  • Out of process. The probe engine (checkAll) is synchronous (which + <bin> --version per tool), and CLAUDE.md is explicit that a sync call freezes the port while the process stays alive. So the route runs codeman doctor --json in a child of the same entry script (process.execPath + process.execArgv + process.argv[1], 30 s timeout) and parses the output. The CLI exits non-zero when a required tool is missing but still prints the report, so a non-zero exit with a valid report is a normal result.
  • The runner is injected (registerDoctorRoutes(app, runner)), so route tests never spawn anything; the default runner's parsing is tested against a faked execFile, and the CLI contract it depends on is tested for real.
  • Admin only in multi-user mode (the report names install paths and versions on the host), with real HTTP status codes (403/400/500).
  • The UI is built with DOM nodes and textContent: paths and labels come from the host.

Tests

  • test/routes/doctor-routes.test.ts (11): envelope, category pass-through and rejection of an unknown one without running anything, runner failure, multi-user gating, and the default runner (args, non-zero exit with a report, empty/non-JSON/wrong-shape output, the child's error passed through).
  • test/doctor-cli-json.test.ts (1): spawns the real entry script with doctor --json --category core and checks the report.
  • test/doctor-settings.browser.test.ts (2, real Chromium, /api/doctor stubbed at the network layer): renders status/version/path/install hint, shows a hostile label (<img onerror>) as literal text, and shows the server's error and re-enables the button.
  • Full CI gate: typecheck, lint, format, public assets, catalogue and all unit/integration tests pass.

Merge order

Rebased onto 1.34.0. Independent of my other open PRs apart from adjacent insertions in config/test-suites.ts, docs/api-reference.md, routes/index.ts and server.ts (route registration), which will need a trivial rebase for whichever lands second. I will do that as they merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS

opticon454 and others added 2 commits October 5, 2026 07:03
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for this, @opticon454! It surfaces codeman doctor in Settings → System → Diagnostics, backed by a new GET /api/doctor that runs the probe in a child process so the synchronous engine never blocks the server. The out-of-process design, allowlisting the category before it reaches argv, rendering host strings with textContent (with the hostile-label test), and the injected runner in the route tests are all exactly how I would want this built.

A few things before it can go in:

1. Installed CLIs show as missing under a service (src/web/routes/doctor-routes.ts:49, src/utils/dependency-checker.ts:96). The child inherits the server's environment, and the doctor engine only looks on PATH via which. Under systemd/launchd that PATH is minimal, so CLIs installed in ~/.local/bin, ~/.npm-global/bin, nvm and so on come back missing with an install hint. The Run menu still finds them, because the CLI resolvers fall back to each entry's discovery.searchDirs and then a login shell. I reproduced it with my production unit's PATH: every CLI row reads missing, while the same server reports Claude and Codex available at ~/.local/bin through /api/claude/status and /api/codex/status. Could you make the doctor resolve CLI rows the way the run mode does? Preferred: carry cli.discovery.searchDirs onto the CLI rows' PathResolver in cliDependencyEntries(), probe <expanded dir>/<bin> when which misses, and run --version against the resolved absolute path (that also fixes the terminal codeman doctor). If you would rather keep the engine untouched, appending every enabled CLI's expanded searchDirs to the child's PATH in defaultDoctorRunner covers the panel. Please add a test for a CLI found only in a searchDirs entry.

2. Flaky browser test (test/doctor-settings.browser.test.ts:52). page.route() does not reliably intercept requests while the service worker controls the page. When it misses, the request hits the real route (which spawns the vitest worker with doctor --json) and the first case times out. It failed 1 of 2 runs here. Creating the page from browser.newContext({ serviceWorkers: 'block' }) held 10 runs out of 10.

3. Concurrent runs are unbounded (src/web/routes/doctor-routes.ts:37). Each request forks a full Node process. Two tabs or a script can stack them. Please single-flight it (share the in-flight promise per category, or answer 409 CONFLICT while one is running).

4. Hide the group from non-admins (src/web/public/index.html:2832). In multi-user mode a non-admin sees the group but can only get a 403. The other admin-only groups hide themselves (_applyMcpSyncAdminGate and friends, re-applied on the codeman:me event near the end of settings-ui.js). A matching _applyDoctorAdminGate() would keep it consistent.

Smaller things, which I can also do at merge if you prefer:

  • src/web/routes/doctor-routes.ts:80: the documented table maps OPERATION_FAILED to 422 and INTERNAL_ERROR to 500, so for an explicit 500 please use INTERNAL_ERROR. When the 30 s timeout kills the child, a message like "timed out after 30 s" would read better than the raw Command failed: ... line.
  • docs/wiki/Settings-Reference.md (### System): one sentence about the Diagnostics group.

Once 1 to 4 are in, I will take another look and merge. Thanks again, this is a really useful addition.

…ate the group (Ark0N#536 review)

- doctor probes each CLI's discovery.searchDirs when which misses and runs --version on the resolved path, so a service with a minimal PATH no longer reports installed CLIs as missing
- GET /api/doctor shares one in-flight run per category
- Diagnostics group hidden from non-admins in multi-user mode (_applyDoctorAdminGate)
- 500 uses INTERNAL_ERROR; a killed child reports 'timed out after 30 s'
- browser test blocks service workers so page.route() is reliable
- wiki: Diagnostics sentence

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
@opticon454

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. All four are in (db9a394):

  1. Installed CLIs missing under a service: cliDependencyEntries() now carries each CLI's expanded discovery.searchDirs onto its PathResolver. When which misses, the doctor probes <dir>/<bin> and runs --version against the resolved absolute path, so the terminal codeman doctor is fixed too. New tests cover a CLI found only in a searchDirs entry, PATH winning over a search dir, and the registry rows carrying expanded dirs.
  2. Flaky browser test: the page now comes from browser.newContext({ serviceWorkers: 'block' }).
  3. Concurrent runs: single-flighted per category (callers share the in-flight promise), with a test.
  4. Non-admins: added _applyDoctorAdminGate(), called when settings open and on codeman:me.

Smaller items: the 500 now uses INTERNAL_ERROR, a killed child reports timed out after 30 s, and the Settings Reference has a Diagnostics sentence.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS

@Ark0N
Ark0N merged commit fed3a08 into Ark0N:master Oct 6, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 6, 2026
- The doctor now judges candidates like the run mode's resolver: the PATH
  hit, then each search dir, each one version-checked on its own and
  skipped on a mismatch (a wrong `pi`/`grok` on the PATH no longer hides
  the real one in a search dir). A search-dir candidate must be an
  absolute path to an executable regular file, so a relative dir or a
  file without the x bit reads as missing, as it does in the Run menu.
  `isExecutableRegularFile` is exported from cli-executable-resolver.ts
  and reused rather than copied.
- Every doctor probe passes killSignal: 'SIGKILL'; a --version that
  ignores SIGTERM held the probe for its full runtime (15 s vs 5 s
  measured with a TERM-trapping script).
- README no longer claims parity with the Run menu or nvm prefixes.
- The Diagnostics panel marks a missing optional tool with ○, a missing
  required one with ✗, as the terminal doctor does.
- expandSearchDir names its twin, expandHome() in cli-resolver.ts.
- test/doctor-cli-json.test.ts is hermetic: temp HOME, a PATH of only
  `which` and `node`, and a clis.json that drops the registry's absolute
  search dirs, so it never runs the machine's installed agent CLIs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 6, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Merged in 1.35.0, thanks @opticon454! Running the doctor in a child process was the right call, and both review rounds came back fast. Applied on the way in (cf26853):

  • The doctor now judges candidates the way the Run menu's resolver does: the PATH hit first, then each search dir, each one version-checked on its own. So a wrong grok or pi on the PATH no longer hides the real one in its install dir. A search-dir candidate has to be an absolute path to an executable regular file (it reuses the resolver's own isExecutableRegularFile).
  • Probes pass killSignal: 'SIGKILL', so a --version that ignores SIGTERM can't hold the doctor for its whole runtime.
  • A missing optional tool shows ○ instead of ✗ in the panel, like the terminal doctor.
  • The README no longer claims parity with the Run menu, whose login-shell step finds nvm installs that the doctor doesn't.
  • test/doctor-cli-json.test.ts is hermetic now (temp HOME, a PATH with only which and node), so the CI gate never runs the machine's installed agent CLIs.

@opticon454
opticon454 deleted the feat/doctor-in-settings branch October 6, 2026 10:02
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