v0.5.12 — Coverage-blind check fixes + symlink write disclosure
Summary: Several checks reported healthy without covering what they claimed — an identifier collision made the wheel-asset gate blind rather than noisy, the MCP stdout-purity test only stayed green because it exercised one request shape, and the release checker's permissions block silently dropped the scope its own checkout needs. Each now establishes its coverage or fails, and a weekly dependency audit is held to the same standard. Separately, mpg setup and mpg uninstall now disclose every symlinked directory a run writes through; the traversal is unchanged and is still not confinement.
Added
- A weekly dependency audit (
.github/workflows/audit-dependencies.yml) runsuv auditover the dependencies this project actually resolves and opens an issue keyed on the advisory set — so a closed issue about old advisories cannot suppress a new one, and a repeat of the same set cannot open a duplicate. It does not run on pull requests: an advisory published against a transitive dependency has nothing to do with the change under review. The verdict is decided byscripts/check_dependency_audit.py, which fails rather than reporting "nothing found" when it cannot establish what the audit covered — a missing subcommand, an unrecognized JSON shape, or an implausibly small audited set. That guard exists because a clean verdict over the wrong corpus is exactly how this check would fail silently: while it was being designed, a barepip-auditreported no vulnerabilities against its own dependencies rather than this project's. CONTRIBUTING documents how to run and triage it locally. (closes #165)
Fixed
-
The wheel-asset gate compared guide identifiers as bare filename stems, which is only correct while no two categories share one. Measured, the consequence runs the opposite way from what the issue anticipated: a collision does not make the gate noisy, it makes it blind. The expected set shrinks along with whatever the index dropped, so the two agree and the check passes — a guide missing from the shipped wheel would be reported as healthy. A guide moved between categories was invisible to it for the same reason. It now compares
(category, id)pairs derived from the wheel's own paths, which removes the dependency on globally unique stems rather than documenting it. (closes #141) -
The scheduled Python release checker files its follow-up issues with
tier:4-extendandarea:contentalongsideenhancement, so automatically created work lands in the same triage as everything else instead of outside the classification scheme. A test reads the--labelarguments specifically; matching anywhere in the workflow text would have been satisfied by the issue body, which names the same words. Duplicate detection is unchanged. (closes #160) -
The scheduled Python release checker declared only
issues: write, and declaringpermissionsat all drops every scope not listed — including thecontents: readitsactions/checkoutstep needs. Public repositories allow the checkout regardless, which is why ten consecutive weekly runs succeeded and the omission never surfaced; it would have failed the moment this repository went private. Both scopes are now declared, and a test asserts the pair without admitting any other write scope. (closes #163) -
The MCP stdout-purity test judged pollution by recomputing the expected byte count with
json.dumps's defaultensure_ascii=True, while the server serializes withensure_ascii=False. Any non-ASCII character reaching stdout made the two counts diverge and failed the test with no stray output present at all — not a hypothetical, sinceguide_index.pycomposes BAD/GOOD summaries with a→and a realsearch_guidesresponse measures 12 bytes "short" under the old comparison. The test only stayed green because it exercisedtools/listalone. The check now judges the stream's structure directly — every line non-empty, free of surrounding whitespace, a standalone JSON-RPC object carrying aresult/error/methodbody, and the stream terminated by a newline — so it no longer depends on the server's serialization settings, and the test additionally pins which response ids belong on stdout so that a well-formed but extra message is still caught. Falsification tests pin that every pollution shape the byte comparison used to catch still fails, and that a genuinely non-ASCII payload passes. (closes #173) -
mpg setupandmpg uninstallnow announce, once per run before writing anything, when.claudeis a symlink and which directory the writes actually land in. Previously the per-file guards refused a symlinkedsettings.local.jsonbut said nothing about a symlinked.claudedirectory, so the hook settings and the Skills/Rules symlinks all silently went to the link target. The symlink is still followed — refusing would break deliberate "config lives elsewhere" layouts — so this closes the silence, not the traversal; SECURITY.md states plainly that neither the per-file checks nor this disclosure confine the.claudetree.--mcp-only, which writes nothing under.claude, prints nothing. (closes #170) -
The same disclosure now covers every directory a run writes through, not just
.claude. A symlinked.claude/skillsor.claude/ruleswas followed with no note at all — the case that motivated the boundary #170 had to document.mpg setupandmpg uninstallnow walk each write target below the project root, report the outermost symlinked directory on the way, and de-duplicate: a symlinked.claudestill yields one note covering all three targets, whileskillsandrulespointing at different trees yield two, because there genuinely are two destinations. The final path component is skipped — those are mpg's own symlinks, so including them would make a secondmpg setupannounce mpg's own links back. The write targets are supplied by the callers rather than duplicated in the settings module, so there is no second copy of the layout to drift, and a source scan pins that no fourth target has appeared unannounced. Traversal is still not refused, and SECURITY.md still says the disclosure is not confinement. (closes #192)