Skip to content

fix(marketplace): bump withheld pins + CI pin-staleness gate#182

Merged
cdeust merged 3 commits into
mainfrom
fix/marketplace-pin-gate-179
Jul 25, 2026
Merged

fix(marketplace): bump withheld pins + CI pin-staleness gate#182
cdeust merged 3 commits into
mainfrom
fix/marketplace-pin-gate-179

Conversation

@cdeust

@cdeust cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Closes #179 — releasing a downstream plugin doesn't ship it. Delivery is gated by pins in this repo's marketplace.json, and nothing watched them.

Pins bumped (the six-release gap, closed)

  • zetetic-team-subagents 2.29.0 → 2.34.0 (withheld: 2.30.0, 2.31.0, 2.32.0, 2.32.1, 2.33.0, 2.34.0 — including §15, the redaction gates, the Fable loops)
  • cortex-viz 2.6.3 → 2.7.1 (withheld: 2.7.0, 2.7.1)

Eight releases reached zero installs. No error, no warning, no diff — the manifests were internally consistent the whole time, which is why nothing noticed.

Failure classes

Class Detects
PIN_BEHIND_RELEASE github-source pin < that repo's latest release — the class that caused this incident
PIN_BEHIND_TAG local-source pin < this repo's latest semver git tag. Offline, git only. Catches a stale pin even when every manifest agrees with itself — the cdeust/automatised-pipeline#67 incident, which a manifest-vs-manifest check reports green on
SELF_PIN_MISMATCH local-source pin ≠ the plugin's own plugin.json
SERVER_JSON_SPLIT server.json ≠ the primary marketplace pin — the third version-carrying file

Two-sided by design per cdeust/zetetic-team-subagents#52: installed < pin and pin < release are different defects with different owners, and a one-sided check reports false compliance.

Frozen pins are a declared contract

FROZEN_PINS names the deprecation shim (cortex 4.15.0, frozen at the hypermnesia-mcp rename) so "intentionally not advancing" is machine-checked and stated at the use site per §8, rather than passing by accident because two files happen to agree. Manifest coherence is still enforced for frozen entries; only the tag-advance check is skipped.

Degraded modes are explicit, never silent

Network failure, rate limiting and releaseless repos emit NOTICE: … network degraded and the run stays green — a gate that goes red on a GitHub blip gets disabled within a month, and then the six-release gap recurs with the gate nominally in place. The summary line reports that notices occurred, so degraded ≠ verified.

One artifact, not two copies

This file is the canonical copy. cdeust/automatised-pipeline carries a byte-identical copy and diffs against Cortex/main weekly, so the two cannot drift into a version where one is a no-op. Verified identical at time of writing.

Merge this before cdeust/automatised-pipeline#68. Its canonical-drift job curls Cortex/main/scripts/check_marketplace_pins.py, which 404s until this lands, and curl -fsSL fails hard on 404.

Prevention, not just detection

CLAUDE.md gains a Releasing section: the checklist ends at the pin bump plus check_marketplace_pins.py exiting 0. The gate detects a stale pin; the procedure stops one being created.


Completion Ledger (§13.2)

Paths introduced → asserting test

Path / branch Evidence
parse_semver v-prefix and bare test_v_prefix_and_bare
parse_semver non-semver → None test_non_semver_none
semver compared numerically, not lexicographically test_numeric_not_lexicographic (2.9.0 < 2.10.0)
PIN_BEHIND_TAG fires when both manifests agree but the tag is ahead test_incident_replay_both_manifests_stale_tag_aheadfails on the pre-fix code (§13 G2)
PIN_BEHIND_TAG silent when current test_current_pin_green (negative assertion, §13 G4)
repo with no tags → no crash, no flag test_untagged_repo_no_crash_no_flag
frozen pin skips tag check, keeps coherence test_frozen_pin_skips_tag_check_keeps_coherence
PIN_BEHIND_RELEASE fires, with release count test_stale_flagged_with_count
release count unavailable → still flagged test_stale_flagged_count_degraded
pin current / pin ahead → pass test_current_and_ahead_pass
network failure → NOTICE, no red run test_network_failure_degrades_to_notice
repo with no releases (HTTP 404) → notice, not KeyError test_no_releases_repo_is_notice_not_keyerror
unparseable tag → reported, not crashed test_unparseable_tag_reported
SERVER_JSON_SPLIT third leg flagged test_three_way_split_third_leg_flagged
aligned / absent server.json → pass test_aligned_passes_and_absent_passes

§13.1 checklist

Item State
A1 happy path, run output quoted pre-bump: exit 1 naming exactly the two stale pins; post-bump: exit 0
A2 edge cases no tags, no releases, unparseable tag, absent server.json, manifestless shim, frozen pin — each mapped above
A3 every failure arm asserted 4 failure classes + 2 degraded arms, each with a named test; the emission itself asserted, not a downstream side effect
A4 boundary validation GitHub API JSON and git output parsed defensively; malformed input returns a reported condition, never a traceback
A5 invariants exit codes stated in the module docstring: 0 current (or degraded, with NOTICE), 1 stale, 2 environment error
A6 idempotency read-only; repeated runs are identical
B1–B3 concurrency N/A — single-threaded, no shared state
C1 scalability one API call per github-sourced plugin (3), one git tag; bounded by manifest size
C2 resource lifecycle urlopen as a context manager with API_TIMEOUT_S; no leaked handles
C3 hot path N/A — CI-only gate
D1 injection no shell string interpolation; subprocess.run with an argument list
D2 untrusted data GitHub API responses treated as untrusted; missing/malformed fields degrade to a notice
D3 secrets GITHUB_TOKEN read from env, never logged; workflow is permissions: contents: read
E1–E4 interfaces additive; no consumer contract changed. FROZEN_PINS and the canonical-copy coupling are new contracts, declared above and in-file. Cross-platform: git + stdlib only
F1 actionable signal every failure names plugin, pinned version, available version and the release count between them
F2 degraded modes named NOTICE: … network degraded, surfaced again in the summary line
G1 path→test ledger above
G2 regression test fails pre-fix test_incident_replay_both_manifests_stale_tag_ahead
G3 deterministic/isolated tests build throwaway git repos under tmp_path; no fixed paths, no sleeps, no network
G4 negative assertions test_current_pin_green, test_current_and_ahead_pass, test_aligned_passes_and_absent_passes
G5 suite executed 15 unit tests green; full CI green (Lint, Type Check, Build Package, SQLite + Windows suites, Docker smoke, CodeQL)
H1 standards stdlib-only, within size caps, no layer violation
H2 readable one comparison per failure class, no indirection
H3 conventions one language per file; gate isolated under scripts/, tests under tests_py/scripts/
H4 CHANGELOG / docs CLAUDE.md Releasing section added; marketplace description corrected for the hypermnesia-mcp rename
H5 commit hygiene conventional messages; pins, gate and docs separable
H6 CI green all checks green on the pushed tree
H7 boy-scout (§14) marketplace metadata.description was stale on the v4.15.0 rename — corrected here rather than left

Verification quoted

pre-bump:   exit 1 — PIN_BEHIND_RELEASE ×2 (zetetic-team-subagents 2.29.0<2.34.0, cortex-viz 2.6.3<2.7.1)
post-bump:  exit 0 — All marketplace pins current.
tests:      15 tests, OK
canonical:  diff vs cdeust/automatised-pipeline copy → identical

🤖 Generated with Claude Code

https://claude.ai/code/session_016gFqzbbNDwPtFzaoGDti7R

cdeust and others added 2 commits July 25, 2026 00:59
zetetic-team-subagents 2.29.0 -> 2.34.0 (six withheld releases:
2.30.0-2.34.0 incl. the §15 rules, redaction gates, Fable loops) and
cortex-viz 2.6.3 -> 2.7.1 (two withheld releases). Root cause: nothing
compared marketplace pins against downstream release tags, so releases
shipped to zero installs with no signal.

Gate: scripts/check_marketplace_pins.py distinguishes
PIN_BEHIND_RELEASE (release never delivered) from SELF_PIN_MISMATCH
(local pin vs plugin.json), per zetetic-team-subagents#52's two-sided
criterion. Workflow runs on PR/push over the manifest AND weekly cron
(pins go stale by inaction; inaction never opens a PR) +
workflow_dispatch. Verified externally: exit 1 naming both stale pins
pre-bump, exit 0 post-bump; 10 unit tests green.

Closes #179

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…metadata.description

The marketplace name stays cortex-plugins (brand-level umbrella over
four plugins); hypermnesia-mcp is one package inside it, renamed at
the plugin level only over the directory name collision. Stating this
in metadata resolves the naming question at first contact with zero
install breakage (a marketplace rename would orphan every
plugin@cortex-plugins key on every install).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cdeust

cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Assessment

This addresses the real defect. The github-sourced path is the class that caused the incident, check_github_pin compares pin against releases/latest correctly, the semver parse is numeric (the 2.9.0 < 2.10.0 test is the right test to have written), the weekly cron is justified in-comment against the inaction failure mode, and the external verification — pre-bump exit 1 naming exactly the two stale pins, post-bump exit 0 — is the evidence §13 A1 asks for. The two pins are bumped and six withheld releases are now deliverable.

Five clauses from #179 are still open.

1. Fail-open is not implemented (criterion 5) — blocking

latest_release_tag calls urllib.request.urlopen with no exception handling. A rate-limit, a DNS failure, or a GitHub outage raises URLError/HTTPError, propagates out of main, and produces a traceback and a red run.

Criterion 5 asked for the opposite behaviour, in these words: "a rate-limited or offline GitHub API degrades to silence, never to a red build on an unrelated PR. The offline path is tested."

This matters more than it looks because of the trigger set: the workflow runs on every PR touching the manifest, so a transient GitHub blip turns an unrelated PR red, and the standard response to a flaky gate is to stop trusting it — which returns us to manual discipline, the thing the issue exists to remove. Anonymous rate limiting is 60 req/h and the cron is unauthenticated-adjacent if the token is ever absent.

2. Two arms unhandled and untested (criterion 6)

TestPinBehindRelease covers stale / current / ahead / unparseable-tag. Not covered:

  • repo unreachable — the fail-open path above.
  • plugin removed upstream / no releasesapi.github.com/repos/{repo}/releases/latest returns 404 for a repo with zero published releases, and json.load(resp)["tag_name"] would KeyError even if the HTTP error were caught. A downstream repo that only ever tagged without publishing a Release hits this on the first run.

Both were named explicitly in criterion 6.

3. Release count missing from the message (criterion 3)

The message gives name, pin, repo and latest tag. Criterion 3 also asked for "the release count between them" — that is what turns the line from a version mismatch into an impact statement ("six releases withheld" is what made #179 legible, and it is exactly the number the PR body computes by hand).

4. Release procedure not documented or wired (criterion 4)

Nothing in the diff touches a release workflow or docs. Criterion 4 asked to "state explicitly whether the bump is automated (preferred: a repository-dispatch from the downstream release workflow) or manual-with-a-CI-gate, and wire whichever is chosen." Today it is manual-with-a-CI-gate by default rather than by decision, and the cron gives at most a 7-day detection bound rather than delivery.

5. Deprecated cortex pin not audited (criterion 7)

cortex v4.15.0 (./plugins/cortex-deprecated) passes check_self_pin mechanically — its plugin.json also says 4.15.0. Criterion 7 asked for the explicit call: intentionally frozen and recorded as such at the use site per §8, or a third stale pin. A mechanical pass is not the audit.

6. No Completion Ledger

§13.2, and §13.5 makes reconciliation Move 0. The body is a strong summary — better than most — but it is not a ledger: no row per path with its asserting test.

Cross-PR note

cdeust/automatised-pipeline#68 copies this script but its manifest has only a source: "./" plugin, so only check_self_pin runs there and no tag is ever read — that gate passes green on its own incident. Whatever fix lands for items 1–2 here should land there too, and the self-path there needs a real tag comparison. Details in that PR's thread.

Summary

Items 1 and 2 are the ones I would hold this on: a gate that goes red on a network blip and crashes on a releaseless repo will be disabled within a month, and then the six-release gap recurs with the gate still nominally "in place". Items 3–6 are unmet clauses of #179 rather than defects — but per §15.1 they are owed, not deferrable.

…, frozen audit, server.json leg

Addresses the PR #182 review in full:
- fail-open: network errors degrade to NOTICE + exit 0 (tested offline
  path) — a gate that reddens PRs during outages gets disabled and the
  gap recurs with the gate nominally in place
- no-releases arm: releases/latest 404 -> NOTICE, no KeyError
- release counts in both stale messages (releases_between /
  tags_between; degrade to countless message, never None-in-string)
- PIN_BEHIND_TAG: local-source pins compared against the repo's own
  latest semver git tag — detection now ORIGINATES offline, catching
  the both-manifests-stale state (AP #67 replay is the load-bearing
  test)
- SERVER_JSON_SPLIT: the third leg of the three-way split guarded
- frozen-pin allowlist with audit reason: the cortex deprecation shim
  is explicitly frozen at 4.15.0 by design, not silently passing
- release procedure wired: CLAUDE.md Releasing section — the checklist
  ends at the pin, not the tag
- canonical-copy header: AP carries a byte-identical copy with a
  weekly CI drift check against this file
15 unit tests (unittest-style so AP runs them without pytest).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cdeust

cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Review addressed in full (commit pushed) + Completion Ledger (§13.2)

Clause State Evidence
Fail-open on network errors DONE URLError/Timeout/OSError → NOTICE + exit 0; test_network_failure_degrades_to_notice (offline path tested)
No-releases arm (404 → KeyError) DONE latest_release_tag returns None on 404; NOTICE, no KeyError; test_no_releases_repo_is_notice_not_keyerror
Release count in messages DONE releases_between/tags_between; count-degraded arm tested (never a None in the string)
Release procedure wired DONE CLAUDE.md 'Releasing' section: the checklist ends at the pin, not the tag; gate command named
Deprecated cortex pin audited DONE FROZEN_PINS allowlist with sourced audit reason; frozen = tag-advance exempt, manifest coherence still enforced (tested)
One artifact, not two divergent copies DONE Canonical-copy header here; AP carries byte-identical script + tests, weekly CI drift check vs this file
Completion Ledger this comment 15/15 unit tests; gate exit 0 on the bumped manifest; pre-bump exit 1 retained from the original evidence

Also folded earlier: metadata.description disambiguation (cortex-plugins umbrella vs hypermnesia-mcp package).

@cdeust

cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

All six items from the previous review are fixed

Item Resolution
Fail-open (criterion 5) except (URLError, TimeoutError, OSError)NOTICE, no failure; test_network_failure_degrades_to_notice asserts no red run from an outage
Repo with no releases latest_release_tag returns None on HTTP 404; test_no_releases_repo_is_notice_not_keyerror covers the KeyError I flagged
Release count (criterion 3) releases_between + test_stale_flagged_with_count, and test_stale_flagged_count_degraded for when the count itself is unavailable
Release procedure (criterion 4) ## Releasing in CLAUDE.md — the checklist now ends at the pin bump, with the incident cited as the source per §8
Deprecated cortex pin (criterion 7) FROZEN_PINS makes "intentionally frozen" explicit and machine-checked rather than an implicit mechanical pass; coherence still enforced
Duplication across repos Canonical-copy declaration + AP's weekly byte-identity diff — one artifact guarding both repos. Better than what I proposed

test_untagged_repo_no_crash_no_flag and test_aligned_passes_and_absent_passes are additions I had not asked for and are the right instincts.

One thing left: the PR body no longer describes the PR

The body still reads as the first revision. It documents two failure classes and "10 unit tests", while the code now carries PIN_BEHIND_TAG, SERVER_JSON_SPLIT, FROZEN_PINS, releases_between, the fail-open path and roughly 18 tests. The most substantial parts of the change — fail-open, the frozen-pin contract, and the canonical-copy arrangement with AP — are invisible to anyone reading the PR rather than the diff.

That matters beyond tidiness here: FROZEN_PINS and the canonical-copy coupling are contracts other repos depend on, and the PR is where that decision gets recorded. Refresh the body and add the Completion Ledger (§13.2) — with the arms now genuinely covered it is mostly transcription from the test names.

Sequencing

Merge this before cdeust/automatised-pipeline#68. Its canonical-drift job curls Cortex/main/scripts/check_marketplace_pins.py, which 404s until this lands, and curl -fsSL fails hard on 404. Noted on that PR too.

@cdeust
cdeust merged commit 339dcfc into main Jul 25, 2026
15 checks passed
@cdeust
cdeust deleted the fix/marketplace-pin-gate-179 branch July 25, 2026 00: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.

bug: marketplace.json pins withhold downstream releases — zetetic-team-subagents stuck 6 releases back, cortex-viz 3, with no signal

1 participant