v1.2.1 — doctor keeps its own report
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 doctorcould be stopped, crashed, or made to print lines it did not write by what a project holds — and a project can arrive bygit clone, since git carries symlinks andgit add -fputs even a git-ignoredsettings.local.jsonin 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 thatdoctorwrote, and the exit status from the table. Stopped: a matcher like".*" * 200 + "Z"backtracked inrefor more than twenty seconds, andre.compilealone 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 withreon 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. Asettings.local.jsonthat 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: acommandor a link destination the OS will not look up — 100,001 characters, a component past the name limit — raised out ofPath.exists()on 3.11-3.13; it now readsdegraded, 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-dirreached through a directorydoctorcannot 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,NaNorInfinity, more than 1 MiB — now readsunknownrather than ending in a traceback, because Claude Code's parser may accept what Python's refuses; a JSON syntax error is stilldegraded. Read as healthy: a.claudethatdoctorcannot look into — an unreadable directory, a symlink loop — answeredabsenton 3.14, which exits 0, because 3.14'sPath.exists()folds every OSError into False; 3.11-3.13 raised instead. Both now readunknownon 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 isunknownrather than skipped — skipping it haddoctordiagnose a directory further up,$HOMEwhen it was measured. Behind all of that, each channel is diagnosed inside its ownexcept Exception, so a failure nobody has found yet reports that channel asunknownwith the exception named and leaves the rest of the report intact. Lines it did not write (#245): a newline in a registeredcommandput a fabricatedmcp presentline on screen with no terminal involved, and an escape sequence could redraw one. Everythingdoctorprints 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 fromclaude mcp getnow reads as one line with\x0ain it. What--run-interpreterruns, and whateverclaude mcp getstarts to answer for the MCP channel, are outside this; whichclauderuns, and what it runs with, is not —claudeis now looked up and run with the relativePATHentries dropped, because those resolve against the current directory and so may be the project's: aclaudefound only through one is not run (the MCP channel readsunknown), and an npmclaude, a#!/usr/bin/env nodescript, no longer picks up a project'snode_modules/.bin/node. (closes #242, #245, #246)- The README now says that the MCP server
mpg doctorstarts, throughclaude mcp get, may be the project's own: in a folder trusted in Claude Code, a project whose.claude/settings.jsonorsettings.local.jsonapproves its.mcp.jsonservers gets its server started — and its command run, as the user — whendoctorasks. 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, whichdoctorinherits; the MCP paragraph used to say only thatclaude mcp get"starts the server". mpg setup,mpg setup --no-hook, andmpg uninstall— every command that readssettings.local.jsonto 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 holdingNaNorInfinity, which they used to read and write back, is refused as well: neither is JSON. This covers whatjsonitself rejects: nesting shallow enough forjsonand deep enough to exhaust the stack when the settings are copied for merging is not covered.