Skip to content

v1.2.1 — doctor keeps its own report

Choose a tag to compare

@yottayoshida yottayoshida released this 11 Sep 08:01
· 4 commits to main since this release
06d2867

Summary: mpg doctor is the command someone runs because they do not trust their tree, and until now the tree could take its report away: a matcher could make it stop answering (#242), a newline in a registered command could write a fabricated line into the report (#245), and an over-long command ended it in a traceback (#246). Those three, and twelve more of the same shape found while fixing them, now end in a report doctor wrote itself, with the exit status from the table.

Fixed

  • mpg doctor could be stopped, crashed, or made to print lines it did not write by what a project holds — and a project can arrive by git clone, since git carries symlinks and git add -f puts even a git-ignored settings.local.json in a commit. The three reported cases, and nine more of the same shape found while fixing them, now all end in a report: one line per channel that doctor wrote, and the exit status from the table. Stopped: a matcher like ".*" * 200 + "Z" backtracked in re for more than twenty seconds, and re.compile alone is quadratic in branches sharing a prefix — thirty to sixty-eight seconds for a million characters. The portable subset has no groups, classes, or escapes, so its syntax check and its search are now written out directly and run in time proportional to the matcher; both were compared with re on every pattern of length five or less over an eleven-character alphabet — the subset's metacharacters, the newline, and letters from both tool names (177,156 patterns for syntax, 285,754 searches, no disagreement; a sixteen-character sweep during review, against seven subjects, found none either), and a test repeats that to length four. A settings.local.json that is a fifo made the read wait for a writer that never came; it is now opened non-blocking and refused as not a regular file (degraded). Crashed: a command or a link destination the OS will not look up — 100,001 characters, a component past the name limit — raised out of Path.exists() on 3.11-3.13; it now reads degraded, since Claude Code meets the same wall, with the detail saying "does not exist or cannot be reached". That includes a link which compares equal to the bundled source and cannot be followed — <300 characters>/../<source> resolves to the source as a string while the OS refuses the path — which 3.11-3.13 answered with a traceback and 3.14 with "the link is right and there is nothing behind it" and a fix of reinstalling mpg. A --project-dir reached through a directory doctor cannot enter now reads as nothing inspected (exit 2) instead of a traceback on 3.11-3.13. A settings file Python can open but not finish parsing — bytes that are not UTF-8, an integer past the digit limit, nesting deeper than the stack, NaN or Infinity, more than 1 MiB — now reads unknown rather than ending in a traceback, because Claude Code's parser may accept what Python's refuses; a JSON syntax error is still degraded. Read as healthy: a .claude that doctor cannot look into — an unreadable directory, a symlink loop — answered absent on 3.14, which exits 0, because 3.14's Path.exists() folds every OSError into False; 3.11-3.13 raised instead. Both now read unknown on every interpreter, from a lookup that calls something absent only when the OS says nothing is there. The project root is found the same way, so an unreadable marker is unknown rather than skipped — skipping it had doctor diagnose a directory further up, $HOME when it was measured. Behind all of that, each channel is diagnosed inside its own except Exception, so a failure nobody has found yet reports that channel as unknown with the exception named and leaves the rest of the report intact. Lines it did not write (#245): a newline in a registered command put a fabricated mcp present line on screen with no terminal involved, and an escape sequence could redraw one. Everything doctor prints now goes through one function that shows control, format, separator, and surrogate characters as escapes (\x0a, \u202e; an undecodable byte from a link as the byte, \xff), and output to a stream that is not UTF-8 is written as ASCII — under cp1251 an ellipsis is byte 0x85, which is NEL. A consequence: a multi-line error from claude mcp get now reads as one line with \x0a in it. What --run-interpreter runs, and whatever claude mcp get starts to answer for the MCP channel, are outside this; which claude runs, and what it runs with, is not — claude is now looked up and run with the relative PATH entries dropped, because those resolve against the current directory and so may be the project's: a claude found only through one is not run (the MCP channel reads unknown), and an npm claude, a #!/usr/bin/env node script, no longer picks up a project's node_modules/.bin/node. (closes #242, #245, #246)
  • The README now says that the MCP server mpg doctor starts, through claude mcp get, may be the project's own: in a folder trusted in Claude Code, a project whose .claude/settings.json or settings.local.json approves its .mcp.json servers gets its server started — and its command run, as the user — when doctor asks. Measured with Claude Code 2.1.268, in a throwaway home directory: without trust or without that approval the server stays pending and is not started. It is Claude Code's behaviour, which doctor inherits; the MCP paragraph used to say only that claude mcp get "starts the server".
  • mpg setup, mpg setup --no-hook, and mpg uninstall — every command that reads settings.local.json to edit it — waited forever on one that is a fifo, and ended in a traceback on one they could open but not parse; they now refuse both, like any other settings file they cannot safely edit, and refuse one larger than 1 MiB for the same reason. A file holding NaN or Infinity, which they used to read and write back, is refused as well: neither is JSON. This covers what json itself rejects: nesting shallow enough for json and deep enough to exhaust the stack when the settings are copied for merging is not covered.