ci(dependabot): ignore pydantic-core bumps past the pydantic-pinned exact version - #416
Conversation
…xact version pydantic-core is transitive (hypermnesia-mcp -> pydantic -> pydantic-core) and pydantic pins it EXACTLY per release rather than by range: pydantic 2.13.4 (latest stable) declares `pydantic-core==2.46.4`. Verified against PyPI's own metadata that no pydantic release accepts 2.47.0 at any maturity — the only releases past 2.13.4 are pre-releases 2.14.0a1/b1, and both already require `pydantic-core==2.48.0`. 2.47.0 sits in a gap between the two pydantic pin generations with no compatible pydantic release, so the bump PR #333 proposed is unresolvable, not merely unresynced (same class of failure as the existing caio ignore entry above it, PR #322). No advisory affects pydantic-core, so this is not a security hold. PR #333 is closed with this same reasoning in its body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…migration Two follow-ups the CI run on PR #331 caught immediately, both fixed at the source rather than baselined/skipped: - tests_py/infrastructure/test_stdio_late_response.py's test method exceeded the 40-line/method cap after the first extraction pass (the extracted helper itself was still over). Split further: a _RaceFixtures dataclass + _build_race_fixtures() groups the six-value setup into one call, leaving _drive_late_request_scenario() and the test method both under the cap. - pyproject.toml gained exceptiongroup>=1.2.0 in a later edit than the `uv lock` run in the prior commit — CI's `uv lock --check` correctly caught the resulting drift. Re-locked (uv.lock unchanged in content, since exceptiongroup was already pinned transitively at the same version — only the marker condition and provenance changed) and regenerated the 5 requirements/*.txt files whose extras include the dev group. Also adds .craftsmanship.conf (same fix as PR #332/#416, not yet on this branch since it was cut before those merged): the local zetetic-marketplace pre-commit hook has no auto-generated-file detection and flags requirements/*.txt against the generic §4.1 500-line cap. Scoped skip for requirements/, matching the documented auto-generated exception. Distinct from this repo's own CI gate (scripts/check_craftsmanship.py), unaffected by this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
ZETETIC-REVIEW: REQUEST_CHANGES Reviewed at head Move 0 — ledger / seen-defect checkNo un-issued "pre-existing/unrelated" dismissal — this PR is the resolution of a previously-deferred issue (#333), with the issue closed and its number cited. Passes. Independent PyPI verification (per review instruction — not reusing the PR's own claim)I queried PyPI's JSON API myself, not the PR's The PR's central factual claim is false. Both the PR body and the This matters beyond pedantry: coding-standards.md §8 requires every claim to trace to a verified source, and this repo's own review instruction for this PR was explicitly "vérifie les métadonnées toi-même... plutôt que de reprendre l'affirmation" — running that check is what surfaced the error. A future maintainer reading this comment will be told "no compatible pydantic exists at any maturity," which will misdirect any future investigation (e.g., "should we track pydantic pre-releases to unblock pydantic-core" is a live, valid option this comment forecloses on false grounds). Does the practical conclusion still hold despite the wrong evidence?Yes, narrowly. Form of the exclusion (
|
…und it Review finding on PR #416: the prior comment claimed no pydantic release at any maturity accepts pydantic-core 2.47.0. That was checked against only one of the two pre-releases past 2.13.4 (2.14.0b1, which requires pydantic-core==2.48.0) and wrongly generalized to the other. PyPI's own metadata for 2.14.0a1 (not previously queried) shows it requires pydantic-core==2.47.0 exactly -- a real match exists, just on an alpha this repo does not track. The practical conclusion is unchanged (2.13.4, the latest STABLE pydantic, pins pydantic-core==2.46.4 exactly, so the bump to 2.47.0 does not resolve against what this repo actually installs) but the comment now states that reason instead of the false "no match at any maturity" one -- a wrong # source: comment is worse than no comment, since the next person to read it takes it as fact. Also narrows the exclusion from the open `>2.46.4` to `>2.46.4,<2.48.0` (PEP 440 comma-AND syntax, verified against `packaging.specifiers .SpecifierSet`): the open form would also have silently blocked the legitimate 2.48.0 bump the day pydantic's stable line reaches 2.14.0 (matching the beta's pin) -- an exclusion should be no wider than the problem it routes around. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
ZETETIC-REVIEW: APPROVE SummarySecond-round review of Verification performed this round (all against primary sources, no benchmark/test run)
Rules compliance (coding-standards.md)
StakesMedium — dependency-update governance for a transitive package with no direct import in this repo (root cause section confirms IssuesBlockingNone. Non-blockingNone — round-1 finding (false universal claim + open-ended exclusion) is fully resolved and independently re-verified against PyPI, not just re-asserted by the author. VerdictAPPROVE. |
…migration Two follow-ups the CI run on PR #331 caught immediately, both fixed at the source rather than baselined/skipped: - tests_py/infrastructure/test_stdio_late_response.py's test method exceeded the 40-line/method cap after the first extraction pass (the extracted helper itself was still over). Split further: a _RaceFixtures dataclass + _build_race_fixtures() groups the six-value setup into one call, leaving _drive_late_request_scenario() and the test method both under the cap. - pyproject.toml gained exceptiongroup>=1.2.0 in a later edit than the `uv lock` run in the prior commit — CI's `uv lock --check` correctly caught the resulting drift. Re-locked (uv.lock unchanged in content, since exceptiongroup was already pinned transitively at the same version — only the marker condition and provenance changed) and regenerated the 5 requirements/*.txt files whose extras include the dev group. Also adds .craftsmanship.conf (same fix as PR #332/#416, not yet on this branch since it was cut before those merged): the local zetetic-marketplace pre-commit hook has no auto-generated-file detection and flags requirements/*.txt against the generic §4.1 500-line cap. Scoped skip for requirements/, matching the documented auto-generated exception. Distinct from this repo's own CI gate (scripts/check_craftsmanship.py), unaffected by this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… real Addresses REQUEST_CHANGES on #419. 1. BLOCK - _check_plugin_pin recognized only two of the marketplace schema's five source shapes (github, local-path) and silently returned ([], [], None) for the other three (npm, url, git-subdir), all schema-legal (verified live against https://json.schemastore.org/claude-code-marketplace.json), all currently unused by this repo's manifest (verified by reading every plugins[].source in it) but not by the dispatcher's own logic. That silence is exactly the defect class this gate exists to close, in code this same PR introduced. Fixed: a dict source with a recognized- but-unchecked type (npm/url/git-subdir) now fails loudly (UNVERIFIED_SOURCE_TYPE, naming the type and pointing at where to add a checker); any other shape (wrong type, unknown source key) fails loudly too (UNRECOGNIZED_SOURCE). 7 new tests in tests_py/scripts/test_check_marketplace_pins_dispatch.py replay the reviewer's exact npm/url probe plus git-subdir, an unknown dict, and two malformed non-dict/non-str shapes - every one now produces a failure, none pass silently. 2. BLOCK - rebased onto origin/main (e88e4e2). The branch was stale behind #414/#416/#417; check_craftsmanship.py --base origin/main (the exact invocation CI runs) is clean post-rebase. 3. Non-blocking, fixed anyway - the incident-replay tests now execute a frozen, verbatim copy of the pre-fix check_github_pin/check_self_pin logic (git blame: pre-e0661ad9) against the identical historical inputs, asserting it returns the old silent (None, None)/[] BEFORE asserting the current code returns the failure AFTER (frozen copies live in tests_py/scripts/_marketplace_pins_legacy_replay.py). The commit message on e0661ad asserted this replay already happened; it did not - only the new code was exercised, and the "before" was prose. This is what the prose should have described from the start. Incidental fix required to keep this PR's own CI green: bumped zetetic-team-subagents 2.36.0 -> 2.37.0 (a real v2.37.0 tag landed on cdeust/zetetic-team-subagents at 2026-08-10T10:32Z, mid-session, from unrelated work - confirmed via `gh release view`, not assumed). Unrelated to items 1-3 and to this PR's actual subject; flagged here rather than silently folded in. Re-measured after, not before: check_craftsmanship.py --base origin/main clean; check_marketplace_pins.py exits 0 live (one NOTICE, the already- disclosed pending registry entry); ruff check/format clean; pyright 0 diagnostics on every touched file; tests_py/scripts/ 771 passed (up from 764 pre-review), 5 skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(mcp2): migrate off fastmcp onto the native mcp 2.0.0 SDK server mcp 2.0.0 folds FastMCP's decorator API directly into the SDK (mcp.server.mcpserver.MCPServer, the documented successor to fastmcp.FastMCP) and fastmcp-slim (latest, 3.4.5) still declares mcp<2.0, so the two cannot coexist in one lock. Adopting MCPServer resolves the conflict and removes ~24 transitive packages from the lock (keyring, secretstorage, jeepney, jaraco-*, openapi-pydantic, websockets, watchfiles, email-validator, ...). Mechanical surface (7 tool_registry_*.py files, mcp_progress.py, mcp_prompts.py, tool_error_handler.py, handlers/_tool_meta.py): FastMCP -> MCPServer in type annotations and imports; Context/ToolError imports moved to mcp.server.mcpserver / mcp.server.mcpserver.exceptions. Two architectural pieces mcp 2.0.0 has no drop-in equivalent for, each resolved by direct empirical verification against the installed package (not assumed from type signatures): 1. tool_profile_middleware.py — FastMCP's 4-hook Middleware (on_list_tools/on_call_tool/on_list_prompts/on_get_prompt) becomes ONE mcp.server.context.ServerMiddleware.__call__(ctx, call_next), dispatching on ctx.method. Rejecting a tools/call by raising before call_next (the naive port) loses the classified message — it skips MCPServer._handle_call_tool's own isError=True conversion and surfaces as a bare "MCPError: Internal server error". Fixed by constructing and returning a CallToolResult(is_error=True, ...) directly instead — the graceful, model-visible shape the original design intended (tool_error_handler.py's whole idiom). prompts/get has no in-band error shape, so that path still raises, but as mcp.shared.exceptions.MCPError specifically (the one class the dispatcher's error mapper recognizes and preserves the message for). Registered via MCPServer(middleware=[...]) at construction — no post-construction add_middleware() exists anymore. 2. stdio_transport.py (295 lines, the FastMCP-race workaround) — DELETED, not ported. Reproduced the exact scenario from its own removed regression test (deterministic anyio.Event synchronization, no sleeps) directly against bare mcp==2.0.0 in an isolated venv: the rewritten dispatcher (mcp.shared.jsonrpc_dispatcher .JSONRPCDispatcher.run) explicitly wraps the write-stream close OUTSIDE the task-group join, so a request still in flight at EOF always gets an explicit answer now -- the original silent-drop defect is fixed upstream. What is NOT reproduced: in-flight work is cancelled on EOF (not drained to completion), so the answer is a graceful CONNECTION_CLOSED error, not the handler's real result -- a narrower but still-correct guarantee (no silent drop, ever) this session initially over-claimed as full parity before checking the actual response payload, not just its presence. Replaced with test_stdio_late_response.py, a positive regression pin for the verified (not assumed) contract. __main__.py's main() is now anyio.run(mcp.run_stdio_async) -- the SDK also owns lifespan entry internally now, and there is no more banner/PyPI-update-check ceremony to preserve (mcp 2.0.0 does neither). Two further defects found only by full empirical round-trips, not by reading signatures: - mcp 2.0.0's MCPServer.tool() has no output_schema parameter at all -- structured output is derived exclusively from the wrapped function's return TYPE ANNOTATION. Every tool_registry_*.py inner function returned bare `-> dict`, which mcp 2.0.0 does not structure (no Tool.output_schema, no structuredContent) -- a universal regression across all 43 tools, not just the 9 with a hand-authored outputSchema. Fixed at the root: `-> dict` -> `-> dict[str, Any]` (mechanical, 43 occurrences) restores auto-derived structuredContent; a new apply_output_schemas() (handlers/_tool_meta.py) then overrides the auto-derived generic schema with each handler's hand-authored outputSchema by assigning tool.output_schema directly on the persistent internal Tool object (a cached_property, overridable via normal instance-__dict__ precedence) -- verified this persists across every subsequent tools/list call, unlike mutating the per-call wire-level mcp.types.Tool the old apply_param_docs() read from (also fixed to read the same persistent internal registry). - mcp 2.0.0's func_metadata no longer parses Google-style docstring Args: sections for prompt-argument descriptions (verified: no such logic anywhere in mcp.server.mcpserver.utilities.func_metadata). mcp_prompts.py's three prompt functions now declare descriptions via Annotated[str, Field(description=...)] instead. Test suite: 7046 of 7046 non-PG-gated tests pass (verified in an isolated venv with the full stack including flashrank installed); 264 skip (PostgreSQL-gated, expected without a live PG). mcp.Client's in-process transport wraps server errors in ExceptionGroup (anyio task-group teardown) where fastmcp.Client raised directly -- test assertions updated to unwrap; tests_py/test_mcp_prompts.py's helper needs ExceptionGroup/BaseExceptionGroup, 3.11+ builtins, so exceptiongroup>=1.2.0 (already a transitive anyio dependency) is now declared directly in the dev extras rather than relied on by luck. uv.lock + requirements/*.txt regenerated via `uv lock` + scripts/generate_pip_constraints.py. .craftsmanship-baseline.json surgically pruned (3 entries) for the deleted stdio_transport.py test files only -- not regenerated wholesale, to avoid grandfathering unrelated pre-existing debt the full-tree --write-baseline scan would otherwise have swept in from files this change never touches. Builds on investigation from an abandoned 2026-08-01 session (commits ba0821b8/86158b0d on the local feat/mcp-2.0 branch, never merged) that correctly identified both hard problems but did not verify its own tool_profile_middleware.py port against a real mcp.Client round-trip (it raises MCPError for tools/call too, which IS recognized and DOES preserve the message -- but changes tools/call rejections from a graceful isError=True result to a protocol-level error, a real, unverified behavior change) and did not reach stdio_transport.py at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(mcp2): keep the craftsmanship gate and lockfile in sync with the migration Two follow-ups the CI run on PR #331 caught immediately, both fixed at the source rather than baselined/skipped: - tests_py/infrastructure/test_stdio_late_response.py's test method exceeded the 40-line/method cap after the first extraction pass (the extracted helper itself was still over). Split further: a _RaceFixtures dataclass + _build_race_fixtures() groups the six-value setup into one call, leaving _drive_late_request_scenario() and the test method both under the cap. - pyproject.toml gained exceptiongroup>=1.2.0 in a later edit than the `uv lock` run in the prior commit — CI's `uv lock --check` correctly caught the resulting drift. Re-locked (uv.lock unchanged in content, since exceptiongroup was already pinned transitively at the same version — only the marker condition and provenance changed) and regenerated the 5 requirements/*.txt files whose extras include the dev group. Also adds .craftsmanship.conf (same fix as PR #332/#416, not yet on this branch since it was cut before those merged): the local zetetic-marketplace pre-commit hook has no auto-generated-file detection and flags requirements/*.txt against the generic §4.1 500-line cap. Scoped skip for requirements/, matching the documented auto-generated exception. Distinct from this repo's own CI gate (scripts/check_craftsmanship.py), unaffected by this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(mcp2): update the remaining hand-maintained fastmcp references Real bugs surfaced by CI running against the mcp 2.0.0 migration, not speculative cleanup: - scripts/launcher_pins.py: BASE_PACKAGES still pinned fastmcp==3.4.5 -- the plugin-bootstrap package list has no resolver available to catch drift itself, which is exactly why tests_py/scripts/test_launcher_pins_match_lock.py exists (compares this list against requirements/setup.txt on every PR) and is what caught this: `_BASE_PACKAGES disagrees with setup.txt`, failing every Test (Python 3.1x) leg on PR #331. Fixed to mcp==2.0.0. - scripts/setup.py: install_deps()'s hand-written package list had the same fastmcp>=2.0.0 entry, unguarded by a test (no equivalent to the launcher_pins check exists for this file). Fixed to mcp>=2.0.0 -- otherwise a fresh `setup.sh` run would install a package this repo no longer imports. - scripts/verify_mcp_hosts.py: refactored main() (Fowler 2018 Ch. 6, Extract Function) into _build_parser/_resolved_command/_case_command/ _run_one_case/_run_all_cases while touching this file for the PYTHONPATH fix -- the previous single function was already at the file's craftsmanship baseline (76 lines, nesting depth 5, both over CLAUDE.md's 40-line/3-level caps) and this PR's own edit was the trigger to fix it rather than add to it further. Both baseline entries pruned since neither violation reproduces anymore. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(mcp2): shrink verify_mcp_hosts.py functions back under the size caps The prior commit's refactor and PYTHONPATH-fix comment pushed two functions over CLAUDE.md's 40-line/method cap again (caught by CI's Craftsmanship Gate on push, not locally -- the pre-commit hook and the CI gate agree, this was a same-session miss, not a policy gap): - _environment(): the PYTHONPATH rationale moved from a 17-line inline comment to the module docstring's new "Environment isolation" section, leaving a two-line pointer at the call site. - _build_parser(): split into _add_selection_arguments (which host identities/profiles a run exercises) and _add_runtime_arguments (how each case is driven) -- the same split the module's own two concerns already implied. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(mcp2): drain before signalling shutdown in the stdio host harness PR #331's red CI was not a Cortex regression and not a race the mcp 2.0.0 SDK gets wrong: it was our harness asking for a guarantee the MCP protocol never grants. Model. Order the events of one batch: arrive(r) -> accept(r) -> begin(r) -> produce(r) -> enqueue(r) -> emit(r), against EOF -> readclose -> cancel -> join -> wclose. Serial reads over rendezvous streams force accept(r) -> EOF -> cancel for every request in the batch, and enqueue(r) -> emit(r) holds because stdout_writer lives outside the dispatcher's task group. Nothing orders enqueue(r) before cancel. The guarantee that is therefore nowhere established -- and that the harness assumed -- is the ANSWER OBLIGATION FOR AN ACCEPTED REQUEST: for every r the dispatcher takes off the read stream, exactly one frame carrying r's id reaches the wire. Mechanism, read from the pinned wheel (sha256 1cb4c75d...49d6, the hash uv.lock pins), not from its docs: JSONRPCDispatcher._handle_request sets answer_write_started on the line BEFORE awaiting the response write, and the shutdown arm declines to send CONNECTION_CLOSED whenever that flag is set ("prefer possibly-zero answers over possibly-two"). A write cancelled at MemoryObjectSendStream.send's entry checkpoint provably never delivered, yet counts as possibly sent -- so the request settles with no frame at all. Instrumented trace: begin(5) write_result-enter(5) send-cancelled-at-entry-checkpoint(5,CancelledError), then silence. This corrects the prior hypothesis (a handler task cancelled before its first scheduling turn); the handler ran to completion. Verdict. The SDK promises nothing here, and MCP 2025-06-18 §Lifecycle > Shutdown > stdio makes closing stdin the shutdown signal with no drain phase defined, so an accepted request is owed nothing once EOF lands. The defect is in the client: subprocess.run(input=...) signals shutdown before reading a single response. Reproduced against a BARE mcp 2.0.0 server with zero Cortex code -- ids 4 and 5 of a six-frame batch got no frame, 5/5 runs; the same batch drained first, 5/5 runs, loses nothing. Change. scripts/mcp_host_client.py owns the exchange and keeps stdin open until every expected id has arrived, closing it only then; synchronisation is by event (a response line, or stdout EOF), never by elapsed time -- the caller's --timeout survives only as a watchdog that kills a wedged child and never decides a verdict. stderr goes to a file, not an undrained pipe. verify_mcp_hosts.py drops to 276 lines, so its baselined file-size entry is pruned. mcp_server/__main__.py's main() comment claimed mcp 2.0.0 had the drain invariant built in; it does not, and the comment now says why the workaround is still not restored (a drain in our transport would let a wedged handler hold shutdown hostage, and real hosts never need it). Tests. test_stdio_eof_drain.py forces both orderings deterministically over one in-memory pair -- park the response write, then EOF: no frame; drain, then EOF: every real result. test_mcp_host_client.py pins that stdin is provably still open at each response read. test_stdio_late_response.py's docstring generalised one green interleaving into "no silent drop, ever"; that inference is retracted, with the scope it actually covers spelled out. Co-Authored-By: Claude <noreply@anthropic.com> * fix(mcp2): follow the harness split through its callers, and type the gate Two CI failures on the previous commit, both real: - tests_py/scripts/test_verify_mcp_hosts.py still imported `_environment` and `_responses` from scripts/verify_mcp_hosts.py, which no longer owns them. Retargeted at scripts/mcp_host_client.py; `_responses(stdout: str)` became `absorb(line, responses)` because the exchange reads one line at a time -- it must decide the batch is answered before it may close stdin, so it cannot wait for the whole stream. Added the case that change introduces: a blank line (the trailing newline whole-stream `splitlines()` never yielded) is not a malformed frame. - The sibling import is now `scripts.mcp_host_client` under one canonical name, with the repo root put on sys.path when this module runs as a script. A bare `import mcp_host_client` would give the executed script and an importing test two distinct module objects -- and two distinct `ContractError` classes, so `pytest.raises` would miss the real one. That dual-identity trap is already documented in tests_py/scripts/_craftsmanship_support.py for its own siblings. Also fixed, pre-existing on this branch and blocking the Type Check gate: mcp_server/tool_profile_middleware.py read `params["name"]` as `Any | None` and handed it to `allows()`/`is_available()`, both typed `str`. `_requested_name` now collapses a missing or non-string name to `""` -- which no profile lists, so both gates refuse it exactly as they already refused an unknown name. Behaviour is unchanged on every input; only the type is now honest. Co-Authored-By: Claude <noreply@anthropic.com> * test(mcp2): pin the drain rule's degenerate cases The exchange's termination argument has to hold at the corners, not only on the happy batch: a server that answers nothing (EOF before any frame) must report nothing and close, never block; a frame arriving after the last awaited id must still be parsed and contract-checked rather than discarded by the loop's exit; and ids are awaited as a SET, since mcp 2.0.0 runs handlers concurrently and does not answer in request order. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: cdeust <cdeust@icloud.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…rsions ship silently (#419) * fix(marketplace): close the pin-verification gap that let dangling versions ship silently The hypermnesia-mcp-viz marketplace pin read version "3.0.0" for six days with no matching cortex-viz tag (v2.8.0 was, and remained, the latest real one). check_marketplace_pins.py never caught it: PIN_BEHIND_RELEASE only ever asked "is the pin behind the latest known tag?" — a pin sitting AHEAD of every real release read as current and passed silently. Root cause, fixed at the source: add PIN_VERSION_UNPUBLISHED (existence, not staleness) for both github-source and self-source pins, and the same principle for a third version surface this incident exposed — the public MCP registry (io.github.cdeust/hypermnesia-mcp was published at 4.17.1 while the tag/server.json/PyPI were already at 4.17.2, invisible until queried directly): REGISTRY_VERSION_STALE, exact-equality against server.json's own declared version. - scripts/check_marketplace_pins.py split into a thin composition-root facade + 5 single-purpose modules (marketplace_pins_{http,semver,github, self,manifests,registry}.py) — the single file crossed the 300-line §4.1 cap once REGISTRY_VERSION_STALE was added. AP's byte-identical mirror needs the same split; its weekly diff job will show that until synced, which is the intended signal, not a regression. - PENDING_PINS / PENDING_REGISTRY: a real, correctly-flagged finding whose fix is genuinely in flight (a real open PR, or a workflow that only fires on the next tag) degrades to a named, non-silent NOTICE instead of a red run — never a placeholder, always naming the tracking reference, dead code once the real state catches up. - Corrected an unverified hypothesis relayed from another session: the marketplace schema does NOT accept only a raw sha. Checked against https://json.schemastore.org/claude-code-marketplace.json (2026-08-10): a github-source pin's `ref` field ("Git branch or tag to use") is schema-legal. The hypermnesia-mcp-viz pin now carries `ref: "v3.1.0"` alongside `sha` (kept for immutability — a tag ref alone can be force-moved after the fact; PIN_SHA_UNREACHABLE verifies it independently) so the pin self-documents its target. - Bumped the pin to the real cortex-viz v3.1.0 release (052e4a40d3e6bddaeb1cec6662e23b451575c481, cdeust/cortex-viz#130): merged, tagged, PyPI-published (verified live on pypi.org 2026-08-10). Supersedes Cortex#418, whose sha (064e6d1) was a provisional PR-head value that changed on squash-merge, per that PR's own note. - Corrected three places in this repo that repeated the same "renamed in v3.0.0" claim cortex-viz's own CHANGELOG made (README.md, marketplace.json's cortex-viz shim description, plugins/cortex-viz-deprecated/hooks/hooks.json's live session-start notice, docs/codex-plugin.md) plus this repo's own CHANGELOG.md Unreleased entry (still in-progress prose, not settled history, so corrected in place rather than annotated). tests_py/scripts/ test_codex_plugin_contract.py had hardcoded the dangling "3.0.0"/sha as its expected contract — fixed to the real v3.1.0/sha. - Adds .github/workflows/release.yml::publish-mcp-registry: auto-publish server.json to registry.modelcontextprotocol.io on every v* tag via `mcp-publisher login github-oidc` (GitHub OIDC, no stored secret — checked against modelcontextprotocol/registry's own docs, not assumed to mirror PyPI Trusted Publishing). Checksum-pinned mcp-publisher v1.8.1 binary. Uncovered and fixed a real blocker while wiring this: server.json's description was 113 chars against the registry's 100-char schema limit (`mcp-publisher validate` 422) — shortened to 98. The actual registry publish of 4.17.2 is NOT done by this PR: it requires either the next v* tag (this job) or a maintainer with real mcp-publisher access — an interactive OAuth/PAT login attempt was correctly refused by this agent's own permission classifier, which is the right outcome for a sensitive, irreversible action taken by an autonomous agent. Test: a dedicated regression replays the incident's exact historical values (repo tags topping out at v2.8.0, pin "3.0.0") through both the pre-fix and post-fix check_github_pin — silently (None, None) before, PIN_VERSION_UNPUBLISHED after. 38 tests across 3 new + 1 modified test file; full tests_py/scripts/ suite green (764 passed, 5 skipped). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(marketplace): close dispatch silence, rebase, and make the replay real Addresses REQUEST_CHANGES on #419. 1. BLOCK - _check_plugin_pin recognized only two of the marketplace schema's five source shapes (github, local-path) and silently returned ([], [], None) for the other three (npm, url, git-subdir), all schema-legal (verified live against https://json.schemastore.org/claude-code-marketplace.json), all currently unused by this repo's manifest (verified by reading every plugins[].source in it) but not by the dispatcher's own logic. That silence is exactly the defect class this gate exists to close, in code this same PR introduced. Fixed: a dict source with a recognized- but-unchecked type (npm/url/git-subdir) now fails loudly (UNVERIFIED_SOURCE_TYPE, naming the type and pointing at where to add a checker); any other shape (wrong type, unknown source key) fails loudly too (UNRECOGNIZED_SOURCE). 7 new tests in tests_py/scripts/test_check_marketplace_pins_dispatch.py replay the reviewer's exact npm/url probe plus git-subdir, an unknown dict, and two malformed non-dict/non-str shapes - every one now produces a failure, none pass silently. 2. BLOCK - rebased onto origin/main (e88e4e2). The branch was stale behind #414/#416/#417; check_craftsmanship.py --base origin/main (the exact invocation CI runs) is clean post-rebase. 3. Non-blocking, fixed anyway - the incident-replay tests now execute a frozen, verbatim copy of the pre-fix check_github_pin/check_self_pin logic (git blame: pre-e0661ad9) against the identical historical inputs, asserting it returns the old silent (None, None)/[] BEFORE asserting the current code returns the failure AFTER (frozen copies live in tests_py/scripts/_marketplace_pins_legacy_replay.py). The commit message on e0661ad asserted this replay already happened; it did not - only the new code was exercised, and the "before" was prose. This is what the prose should have described from the start. Incidental fix required to keep this PR's own CI green: bumped zetetic-team-subagents 2.36.0 -> 2.37.0 (a real v2.37.0 tag landed on cdeust/zetetic-team-subagents at 2026-08-10T10:32Z, mid-session, from unrelated work - confirmed via `gh release view`, not assumed). Unrelated to items 1-3 and to this PR's actual subject; flagged here rather than silently folded in. Re-measured after, not before: check_craftsmanship.py --base origin/main clean; check_marketplace_pins.py exits 0 live (one NOTICE, the already- disclosed pending registry entry); ruff check/format clean; pyright 0 diagnostics on every touched file; tests_py/scripts/ 771 passed (up from 764 pre-review), 5 skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(marketplace): sequence registry publish after PyPI; pin zetetic-team-subagents sha Three more items from review on #419. 1. REFUSED, with evidence — the request was to replace "ai-architect-mcp- codebase" with "automatised-pipeline" in check_marketplace_pins.py's docstring (now marketplace_pins_manifests.py), citing an AP canonical- drift job failure. Direct verification (curl raw.githubusercontent.com/ cdeust/Cortex/main/... + gh api against ai-architect-mcp-codebase's copy, both fetched and diffed this session) shows the OPPOSITE of the claim: Cortex main already reads "ai-architect-mcp-codebase" (the current name); AP's own copy still reads "automatised-pipeline" (the old one). Making the requested edit would reintroduce the exact half- rename the instruction itself warned against. AP's repo needs the fix, not Cortex's — out of scope for this PR. No change made here. 2. zetetic-team-subagents pin hardened the same way hypermnesia-mcp-viz already was: added `ref: "v2.37.0"` + `sha` (57a5723d..., verified via `gh api .../git/refs/tags/v2.37.0` and confirmed `identical` to `main` via the compare endpoint — not a dangling PR-head). 3. `publish-mcp-registry` re-sequenced: `needs: publish-pypi` (was `needs: build`, parallel to `publish-pypi` rather than after it). A registry entry naming a version before the PyPI package exists is the same PIN_VERSION_UNPUBLISHED-shaped defect this PR spends most of its diff closing, just pointed the other direction — nearly reintroduced it in the very job meant to fix the analogous drift. Verified against cortex-viz's own Release.yaml (`needs: [test, release]` on its publish-registry job, fetched and read this session), same rationale. Re-measured after: check_craftsmanship.py --base origin/main clean; check_marketplace_pins.py exits 0 live; actionlint clean on release.yml; ruff check/format clean repo-wide; tests_py/scripts/ 771 passed, 5 skipped (unchanged — no test asserts the two JSON/YAML-only edits' exact values, appropriately, since neither introduces new logic). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Closes #333 (
pydantic-core2.46.4 -> 2.47.0) as genuinely unresolvable, not merely unresynced, and adds adependabot.ymlignore entry so it stops reopening.Root cause
pydantic-coreis never declared directly inpyproject.toml— it arrives transitively throughpydantic, which pins its Rust extension exactly per release (ABI lockstep), not by a compatibility range:pydantic2.13.4 (latest stable, no newer stable release exists) declarespydantic-core==2.46.4.2.14.0a1/2.14.0b1, and both already requirepydantic-core==2.48.0.pydantic-core2.47.0 sits in the gap between those twopydanticpin generations — nopydanticrelease, stable or pre-release, declares compatibility with it. This is the same failure class as the existingcaiodependabot-ignore entry a few lines above it independabot.yml(PR #322): a transitive package whose owner pins it exactly, where the proposed version has no compatible owner release at all.Verified against PyPI's own
requires_distmetadata (not againstuv lock's resolver output alone):No security advisory affects
pydantic-core(gh api "/advisories?ecosystem=pip&affects=pydantic-core"returns[], checked 2026-08-10), so staying on 2.46.4 is not a security hold.Fix
.github/dependabot.yml: added apydantic-coreignore entry (versions: [">2.46.4"]) with the reasoning above and its sources inline, mirroring the existingcaioentry's format. Remove it oncepydantic's stable line moves past 2.13.x and pins a newerpydantic-core.Verification
scripts/check_craftsmanship.py— OKruff check/ruff format --check— cleanyaml.safe_load) and the newignoreentry round-trips correctlyCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com