Skip to content

fix(manifest): bump own pins 0.8.0 → 0.8.2 + CI pin-staleness gate - #68

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

fix(manifest): bump own pins 0.8.0 → 0.8.2 + CI pin-staleness gate#68
cdeust merged 3 commits into
mainfrom
fix/manifest-pin-gate-67

Conversation

@cdeust

@cdeust cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Closes #67 — releasing does not ship. Installs subscribe through .claude-plugin/marketplace.json, and this repo's manifests sat at 0.8.0 while server.json and the latest tag were at v0.8.2 — a three-way split that delivered v0.8.1 and v0.8.2 to zero installs, with no error anywhere.

The incident, and why the first cut of this PR did not catch it

A gate that only compares marketplace.json against plugin.json reports green on the incident: both files were stale at 0.8.0, so they agreed with each other. Nothing read the tag. The gate now reads the latest local git tag (PIN_BEHIND_TAG, offline, git only), so it can originate the detection rather than confirm a human's.

Failure classes

Class Detects
PIN_BEHIND_TAG local-source pin < this repo's latest semver git tag — the #67 incident, even when every manifest agrees with itself
PIN_BEHIND_RELEASE github-source pin < that repo's latest release (the cdeust/Cortex#179 class)
SELF_PIN_MISMATCH local-source pin ≠ the plugin's own plugin.json
SERVER_JSON_SPLIT server.json ≠ the primary marketplace pin — the third leg of the split

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

One artifact, not two copies

scripts/check_marketplace_pins.py is a byte-identical copy of the canonical file in cdeust/Cortex. A cron-only canonical-drift job diffs against it weekly, so the two repos cannot diverge into a version where one is a no-op. Cron-only is deliberate: PR runs must not depend on the sibling repo's merge state.

Merge order: cdeust/Cortex#182 must land first. canonical-drift curls Cortex/main/scripts/check_marketplace_pins.py, which 404s until that PR merges, and curl -fsSL fails hard on 404.

Prevention, not just detection

CONTRIBUTING.md gains a Releasing section: the checklist ends at python3 scripts/check_marketplace_pins.py exiting 0, and names all four version-carrying files (marketplace.json, plugin.json, server.json, Cargo.toml). The gate catches a stale pin after the fact; the procedure is what stops it being created (#67 criterion 3).


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 on the real incident 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 (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 python3 scripts/check_marketplace_pins.pyAll marketplace pins current. exit 0
A2 edge cases no tags, no releases, unparseable tag, absent server.json, manifestless local dir — 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 side effect
A4 boundary validation GitHub API responses and git output parsed defensively; malformed input returns a reported condition, never a traceback
B1–B3 concurrency N/A — single-threaded script, no shared state
C1 scalability one API call per github-sourced plugin (currently zero in this repo), one git tag invocation; bounded by manifest size
C2 resource lifecycle urlopen used 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 JSON treated as untrusted — missing/malformed fields degrade to a notice
D3 secrets GITHUB_TOKEN read from env, never logged
E1–E4 interfaces additive; no consumer contract changed. Cross-platform: git + stdlib only
F1 actionable signal every failure names plugin, pinned version, available version and release count
F2 degraded modes named NOTICE: … network degraded is explicit, and the summary line reports that notices occurred
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
G4 negative assertions test_current_pin_green, test_current_and_ahead_pass, test_aligned_passes_and_absent_passes
G5 suite executed, output quoted python3 -m unittest scripts/tests/test_check_marketplace_pins.pyRan 15 tests … OK; cargo fmt --check and cargo clippy -D warnings clean
H1 standards stdlib-only, no file over the size caps, no layer violation
H2 readable two-value comparison per class, no indirection
H3 conventions one language per file; the Python gate is isolated under scripts/
H4 CHANGELOG / docs CONTRIBUTING.md Releasing section added
H5 commit hygiene logic commit separate from the bytecode/ignore/docs commit
H6 CI green pin gate SUCCESS; canonical-drift SKIPPED by design on PR (cron-only)
H7 boy-scout (§14) two committed .pyc files removed and __pycache__/ + *.py[cod] added to .gitignore — seen during review, fixed here, not deferred

Verification quoted

$ python3 -m unittest scripts/tests/test_check_marketplace_pins.py
Ran 15 tests in 4.844s
OK

$ python3 scripts/check_marketplace_pins.py
All marketplace pins current.
exit=0

$ versions   marketplace.json=0.8.2  plugin.json=0.8.2  server.json=0.8.2  Cargo.toml=0.8.2   tag=v0.8.2

🤖 Generated with Claude Code

https://claude.ai/code/session_016gFqzbbNDwPtFzaoGDti7R

)

Both .claude-plugin manifests (marketplace.json pin AND plugin.json)
lagged the v0.8.2 tag — the release flow bumps Cargo.toml/server.json
but never touched these. Gate: scripts/check_marketplace_pins.py
(PIN_BEHIND_RELEASE + SELF_PIN_MISMATCH, two-sided per ZTS#52) +
weekly-cron workflow, pattern from Cortex #179. Externally verified:
the gate caught plugin.json 0.8.0 AFTER the marketplace bump — a
second stale file the audit had not listed — then exit 0 with both
fixed.

Closes #67

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

cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

The gate cannot detect the defect it was written for

.claude-plugin/marketplace.json on this branch has exactly one plugin, source: "./". That routes to check_self_pin, which compares the marketplace pin against .claude-plugin/plugin.json. The latest release tag is never consulted on this path.

Replay the incident from #67 against this code:

File Pre-PR value
.claude-plugin/marketplace.json 0.8.0
.claude-plugin/plugin.json 0.8.0
latest release tag v0.8.2

check_self_pin finds actual == pin → returns Noneexit 0, green. The gate passes on the exact state that motivated the issue.

The PR body says the gate "proved itself during authoring: after bumping the marketplace pin, it flagged .claude-plugin/plugin.json still at 0.8.0". That is accurate but it is detection of the second file after a human had already found and fixed the first. The gate can only ever report internal disagreement between two manifests; it cannot originate the detection, because nothing in this repo's path reads a tag. #67 criterion 2 asked specifically for "a local tag comparison" — git tag/gh release view against the manifest — and that is the one comparison missing.

Corollary: check_github_pin, latest_release_tag and _headers have no caller in this repo (no github-sourced plugins in the manifest), so they are dead code per §9, and GITHUB_TOKEN in the workflow is unused. Either the self-path gains a real tag comparison — which makes the token meaningful — or the unused code goes.

Third version file, uncovered

server.json was already at 0.8.2 while both manifests sat at 0.8.0 — the repo was in a three-way split and this PR reconciles two of the three. server.json (and its packages[].version) is not read by the checker, so the same drift can recur there silently. #67 criterion 5 required exactly this coverage: "a single release must not be able to leave three files disagreeing about what version this is."

No tests

The diff adds scripts/check_marketplace_pins.py (130 lines, branching) with no test file. #67 criterion 4 required every arm asserted with a named test: pin current (silent — negative assertion per G4), pin behind, tag absent/malformed. §13 G1 (path→test ledger) and G5 are unmet. Cortex#182 ships 10 unit tests for the same script; this PR ships none.

No Completion Ledger

Per §13.2 the ledger is mandatory and its absence is REFUSED at Move 0, before other analysis. The body is a good summary but it is not a ledger: no row-per-path, no path→test mapping.

Also unmet from #67

  • Criterion 3 — pin bump wired into the documented release procedure / release workflow. Nothing in the diff touches release.yml or docs, so cutting a tag and delivering it remain two actions, which is the root cause.

What would close this

  1. Compare the self-pin against this repo's latest release tag, not only against plugin.json. That is the criterion-2 check and the one that catches bug: marketplace.json pin lags own release (v0.8.0 vs v0.8.2) — released versions never reach installs #67's incident.
  2. Extend coverage to server.json + packages[].version.
  3. Tests for every arm, including the replayed 0.8.0/0.8.0/v0.8.2 state as a regression test that fails on the pre-fix code (§13 G2).
  4. Delete or wire the currently-dead github path.
  5. Release-procedure wiring (criterion 3).
  6. Completion Ledger.

Item 3 is the one I would not compromise on: without it there is no evidence the gate catches the incident, and the current code demonstrably does not.

The reviewed version only compared marketplace.json vs plugin.json:
both stale together (the actual #67 state: manifests 0.8.0, tag
v0.8.2) passed green. Now byte-identical with the canonical copy
(cdeust/Cortex): PIN_BEHIND_TAG compares local pins against this
repo's own latest semver git tag (offline, originates detection — the
incident replay is the load-bearing test), SERVER_JSON_SPLIT guards
the third leg that sat unguarded, fail-open network arms, release
counts in messages. Dead code eliminated by USE: check_github_pin now
exercised via the shared tests. 15 unit tests run in CI via plain
unittest (no new deps); weekly cron diffs this copy against canonical
so the two can never diverge into a no-op again.

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)

The core hole is closed: detection now ORIGINATES. PIN_BEHIND_TAG compares local-source pins against this repo's own latest semver git tag — offline, no network — and the incident replay (marketplace 0.8.0 + plugin.json 0.8.0 + tag v0.8.2 → red with '2 release(s) never delivered') is the load-bearing test.

Review finding State Evidence
Gate passes on the motivating state FIXED test_incident_replay_both_manifests_stale_tag_ahead
check_github_pin/_headers dead in AP (§9) FIXED by use shared canonical script; functions exercised by the 15-test suite run in CI (plain unittest, no new deps)
server.json third leg unguarded (criterion 5) FIXED SERVER_JSON_SPLIT + tests; AP's server.json (0.8.2) now reconciled with both manifests
Zero tests (§13 G1/G5) FIXED 15 unit tests, byte-identical with Cortex's suite, running in this workflow
Two diverged copies FIXED byte-identical with canonical (cdeust/Cortex); weekly cron drift-diff job (cron-only so this PR doesn't depend on the sibling merge)

Merge order note: Cortex #182 should merge first (it hosts the canonical file the drift check reads).

@cdeust

cdeust commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Confirmed fixed

  • PIN_BEHIND_TAG — the self-path now reads latest_local_tag via git, offline. test_incident_replay_both_manifests_stale_tag_ahead is the regression test that fails on the pre-fix code (§13 G2). This was the blocking finding; it is closed.
  • SERVER_JSON_SPLIT — third leg covered, with test_three_way_split_third_leg_flagged.
  • Tests present, and the dead github path is now live via the shared canonical copy rather than deleted — a better resolution than the one I suggested.

Three items remain, none of them the logic.

1. Build artifacts committed — blocking (§9, §14)

scripts/__pycache__/check_marketplace_pins.cpython-314.pyc
scripts/tests/__pycache__/test_check_marketplace_pins.cpython-314.pyc

Both are in the diff, and .gitignore has no __pycache__ / *.pyc rule — so this recurs on the next Python touch in this Rust repo. Remove the two files and add the ignore rule in the same PR (§14.1: a defect seen in touched material is fixed here, not filed).

2. Merge order: Cortex#182 must land first

canonical-drift fetches raw.githubusercontent.com/cdeust/Cortex/main/scripts/check_marketplace_pins.py. That path 404s right now — the canonical copy only exists in the unmerged cdeust/Cortex#182:

$ gh api repos/cdeust/Cortex/contents/scripts/check_marketplace_pins.py
gh: Not Found (HTTP 404)

curl -fsSL fails hard on 404, so if this merges first the first weekly cron is a red run pointing at a missing file rather than at drift. Not a code defect — just a sequencing constraint worth stating on the PR so it is not rediscovered at 06:00 on a Monday. (The cron-only trigger itself is the right call, and the in-comment justification is sound.)

3. Release procedure still undocumented (criterion 3)

cdeust/Cortex#182 added a ## Releasing section to its CLAUDE.md stating that the checklist ends with the pin bump. This repo has no CLAUDE.md and nothing equivalent in the diff, so cutting a tag and delivering it are still two actions here — the root cause of #67. The gate now catches the gap after the fact (good), but criterion 3 asked for the procedure that prevents it. README or CONTRIBUTING is fine as the home.

Ledger

Still no Completion Ledger (§13.2). With the arms now genuinely covered this is mostly transcription, but it is the artifact the reviewer reconciles against.

…ease procedure

Review round 2 on #68 — three non-logic items.

- Remove scripts/__pycache__/*.pyc and scripts/tests/__pycache__/*.pyc from
  the index. Build artifacts (§9); they entered because this Rust repo now
  carries a Python gate and nothing ignored bytecode.
- .gitignore: __pycache__/ + *.py[cod], so it cannot recur on the next
  Python touch (§14.1 — fix the defect that was seen, here, not later).
- CONTRIBUTING.md "Releasing": a release is not shipped until its pins move.
  Names the four version-carrying files (marketplace.json, plugin.json,
  server.json, Cargo.toml) and ends the checklist at
  `python3 scripts/check_marketplace_pins.py` exiting 0 — closing #67
  criterion 3, the procedure that prevents the gap the gate detects.

Verified: 15/15 unit tests green via the CI invocation
(`python3 -m unittest scripts/tests/test_check_marketplace_pins.py`);
gate exits 0; all four version files at 0.8.2 matching tag v0.8.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFqzbbNDwPtFzaoGDti7R
@cdeust
cdeust merged commit 1e6337a into main Jul 25, 2026
4 checks passed
@cdeust
cdeust deleted the fix/manifest-pin-gate-67 branch July 25, 2026 00:30
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 pin lags own release (v0.8.0 vs v0.8.2) — released versions never reach installs

1 participant