From b803bba0eb1730cdaa103345003e14928d559a32 Mon Sep 17 00:00:00 2001 From: Florian DAVID Date: Mon, 5 Oct 2026 09:27:32 +0200 Subject: [PATCH] chore: guard trigger 11, validate in the portal before the tag (#472) A case pattern opening on its optional parenthesis holds COMMAND_SCRIPT_NOT_FOLLOWED (found on claude-remember). New suite test-no-paren-case-patterns.sh keeps every shipped script free of it, with awk rules and a stray "in (#417)" proven not to match. docs/releasing.md gains a portal check before the tag, since the listing auto-publishes from a push webhook, and its "not yet observed" claims are checked against the v0.13.0 to v0.16.0 runs. The release.md entry carries the same step. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../jit-context/paths/00-manual/release.md | 7 +- changelog.d/472.added.paren-case-guard.md | 1 + docs/releasing.md | 64 ++++++------- tests/test-no-paren-case-patterns.sh | 90 +++++++++++++++++++ 4 files changed, 129 insertions(+), 33 deletions(-) create mode 100644 changelog.d/472.added.paren-case-guard.md create mode 100755 tests/test-no-paren-case-patterns.sh diff --git a/.claude/jit-context/paths/00-manual/release.md b/.claude/jit-context/paths/00-manual/release.md index 36c0795e..c3ae88f3 100644 --- a/.claude/jit-context/paths/00-manual/release.md +++ b/.claude/jit-context/paths/00-manual/release.md @@ -49,8 +49,9 @@ gh release create v0.2.0 --title "..." --notes "..." 0. **Assemble the changelog and commit it**, before the green check — the assembled `CHANGELOG.md` heading is one of the three sites `tests/test-version-sites.sh` compares, so a tag cut before the fold is a tag whose CI never saw the file it ships. 1. **CI green on the exact commit you are about to tag** — `gh-branch:main` reports the head SHA it judged, so a stale run cannot pass for a current one. -2. **Tag, then push the tag** — `git push` alone does not carry tags. -3. **Create the GitHub release** against that tag, notes from the CHANGELOG section. -4. **Add or update the entry in [`claude-marketplace`](https://github.com/Digital-Process-Tools/claude-marketplace)** — `/plugin install jit-context@dpt-plugins` resolves through `.claude-plugin/marketplace.json` there, not through this repo. The README documents that install command, so until the marketplace entry exists the first instruction a new user follows fails. +2. **Validate the release tree in the portal before the tag.** Build it from the release commit, push it to a throwaway `release-preview` branch, validate `Digital-Process-Tools/claude-jit-context@release-preview` without clicking Next, delete the branch. The listing auto-publishes from a push webhook since 2026-10-04: the tag reaches the directory in minutes and no one looks first. `docs/releasing.md` step 3 has why. +3. **Tag, then push the tag** — `git push` alone does not carry tags. +4. **Create the GitHub release** against that tag, notes from the CHANGELOG section. +5. **Add or update the entry in [`claude-marketplace`](https://github.com/Digital-Process-Tools/claude-marketplace)** — `/plugin install jit-context@dpt-plugins` resolves through `.claude-plugin/marketplace.json` there, not through this repo. The README documents that install command, so until the marketplace entry exists the first instruction a new user follows fails. Marketplace entries name the repo, not a version — users get the default branch. A tag is therefore for humans and the changelog, not a pinning mechanism: whatever is on `main` is what installs. diff --git a/changelog.d/472.added.paren-case-guard.md b/changelog.d/472.added.paren-case-guard.md new file mode 100644 index 00000000..2731834a --- /dev/null +++ b/changelog.d/472.added.paren-case-guard.md @@ -0,0 +1 @@ +- A guard for the Anthropic directory validator's eleventh known trigger, a `case` pattern written with its optional leading parenthesis (`(*x*)`), found on claude-remember. `tests/test-no-paren-case-patterns.sh` fails on one in any shipped script; jit-context has none today. `docs/releasing.md` now validates the release tree in the portal before the tag, since the listing auto-publishes, and its claims about the first real release are checked against v0.13.0 to v0.16.0 (#472). diff --git a/docs/releasing.md b/docs/releasing.md index 00a8213f..1486e02b 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -16,20 +16,17 @@ branches people can install from: | Branch | Who reads it | What decides the version they get | | --- | --- | --- | | `main` | the DPT marketplace, manual installs | `version` in `.claude-plugin/plugin.json` on `main` | -| `release` | the Anthropic directory, once the listing's tracked branch is switched to it (**not done yet for this plugin** -- see "Switching the listing to `release`" below) | the latest commit on `release`, which only the release workflow writes | +| `release` | the Anthropic directory, which has tracked it since the submission of 2026-10-04 (`Digital-Process-Tools/claude-jit-context@release`, v0.16.0) | the latest commit on `release`, which only the release workflow writes | `release` is built by [`.github/workflows/release-branch.yml`](../.github/workflows/release-branch.yml) from a tag. It never shares history with `main`: each release is one commit on top of the previous release commit, and its message names the tag and the `main` commit it came from. -**Nothing in this file has been run against a real tag yet (#437).** The workflow, the -three scripts under `.github/scripts/` and `.github/release-branch.json` were all -exercised locally (`tests/test-release-branch-437.sh`, and a manual -`build_release_tree.py` + `check_release_tree.py` + `smoke_release_tree.py` run against -this branch's own HEAD) before this file was written, but no tag has been pushed and the -directory listing still follows `main`. Facts below marked **observed** come from that -local run; facts marked **not yet observed** are carried over from claude-remember's own -sequence and have not been confirmed against this repository's first real release. +**Run against real tags since v0.13.0.** The tag pushes of v0.13.0, v0.14.0, v0.15.0 and +v0.16.0 each started the `release branch` workflow, which went green and pushed one commit +on `release` (`588ae2f` for v0.16.0). This file was first written before any of that ran +(#437); the claims it carried as **not yet observed** were checked against those four runs +on 2026-10-05 and rewritten where the runs settled them. ## The sequence @@ -44,19 +41,29 @@ sequence and have not been confirmed against this repository's first real releas 2. **Run the full suite (`bash tests/run-all.sh`) and commit the release on `main`.** `.oss.json`'s `release.merge_method` is `"squash"` for this repository (claude-remember - leaves it `null`, i.e. a direct commit) -- **not yet observed**: which shape the first - real release commit here takes depends on which path `release.authority` (`"loop"`) - actually drives it through, and that has not run yet. + leaves it `null`, i.e. a direct commit). **Observed:** the v0.13.0 to v0.16.0 release + commits are direct commits on `main` named `chore(release): x.y.z`, with no pull + request number in the subject. *Why:* that commit is what gets tagged; CI across three OSes (`hooks (ubuntu-latest)`, `hooks (macos-latest)`, `hooks (windows-latest)`, plus `fragment` and `shellcheck` -- all five are required status checks on `main`, observed via the GitHub API) is the gate. -3. **Tag that commit `vx.y.z`** (`.oss.json`'s `release.tag_pattern` is `"v{version}"`) +3. **Validate the tree that tag will publish, in the portal, before you tag.** Build it + (`build_release_tree.py --ref `), push it to a throwaway branch + (`release-preview`), and validate `Digital-Process-Tools/claude-jit-context@release-preview` + in the submission form without clicking Next. Delete the branch after. + *Why:* the listing has auto-publish on and a GitHub push webhook (2026-10-04), so a tag + reaches the directory within minutes and nobody looks at it first. `check_release_tree.py` + and the `tests/test-no-*` suites only know the shapes we already found; the portal is the + only place a new one shows. A hold found here costs a branch, the same hold found after the + tag costs a version stuck in review. `docs/directory-validator.md` has the method, and the + shared notes in `~/Documents/claude-directory-publishing` have every shape seen so far. + +4. **Tag that commit `vx.y.z`** (`.oss.json`'s `release.tag_pattern` is `"v{version}"`) **and push the tag with your own credentials**: `git tag vx.y.z && git push origin vx.y.z`, then check it landed with `git ls-remote --tags origin vx.y.z`. - *Why:* **pushing the tag is what publishes to the directory**, once the listing's - tracked branch follows `release` (not yet true here -- see below). The tag push starts + *Why:* **pushing the tag is what publishes to the directory**. The tag push starts the `release branch` workflow (`on: push: tags: ["v*"]`, confirmed by reading the workflow file), and that workflow is the only thing that writes `release`. No tag, no directory update. @@ -66,7 +73,7 @@ sequence and have not been confirmed against this repository's first real releas checkout with the user's own forge credentials, the same way the oss plugin's release flow does for claude-remember. -4. **Watch the `release branch` run** (Actions tab, or `gh run list --workflow +5. **Watch the `release branch` run** (Actions tab, or `gh run list --workflow release-branch.yml`). It has the same two jobs claude-remember's does -- this workflow was copied and only its header comment and the final commit message's issue number were changed (#437); everything else, including `CLAUDE_CLI_VERSION: @@ -91,8 +98,8 @@ sequence and have not been confirmed against this repository's first real releas *Why two jobs:* the smoke test runs the plugin's own hooks, so it never holds a token that can push. - **Not yet observed: whether `github-actions[bot]` can actually push `release` here.** - Checked without changing anything (#437, via `gh api + **Observed since v0.13.0: `github-actions[bot]` pushes `release` here.** Before the + first run it was checked without changing anything (#437, via `gh api repos/Digital-Process-Tools/claude-jit-context/rulesets` and `.../branches/main/protection`): - **No repository rulesets** (`rulesets` returned `[]`). @@ -112,17 +119,18 @@ sequence and have not been confirmed against this repository's first real releas every repository would not show up in either check above and is the one gap this audit could not close. -5. **Publish the GitHub release** (`scripts/release_publish.py` from the `oss` plugin, +6. **Publish the GitHub release** (`scripts/release_publish.py` from the `oss` plugin, not a file in this repository -- same as claude-remember; confirmed `.oss/` here carries only `assemble_changelog.py`, `statusline.py` and its own `README.md`, no release script). It runs `gh release create --verify-tag`, reading release notes from `CHANGELOG.md` in the maintainer's local `main` checkout, never from the release tree. `.oss.json`'s `release.create_release` is `true`, `draft` is `false`, `latest` is - `true` -- **not yet observed against a real tag**. + `true`. **Observed:** v0.12.0 to v0.16.0 are GitHub releases, v0.16.0 marked Latest. -6. **The directory picks up the new `release` commit** and scans it, once the listing's - tracked branch has been switched to `release` (step not yet taken for this plugin -- - see below). Read the portal's **Versions** tab rather than assuming a waiting version +7. **The directory picks up the new `release` commit** through the push webhook and scans + it. With auto-publish on, a version with no hold goes live without a person. Not yet + observed: what happens to a new version while the first submission is still in review. + Read the portal's **Versions** tab rather than assuming a waiting version is only in a queue; a **Policy hold** (the `UNREAD_ASSET_REFERENCED` / `VALIDATION_INCOMPLETE` findings #437 exists to clear) waits for an Anthropic reviewer regardless of how clean a later version is. @@ -220,13 +228,9 @@ suites run a second time against the tree it builds. Both run in `release-branch The directory follows **one branch or tag**, set in the developer portal (claude.ai/directory/manage, the plugin's page, Settings, Source, "Tracked branch or -tag"). **Not yet switched for this plugin (#437):** claude-remember's own listing moved -to `release` only after that repository's `release` branch already existed and was -checked to hold only the slim tree -- the same order applies here: cut a release through -the sequence above first, confirm `release` exists and is slim, and only then change the -portal field. Left on the default branch (`main`) until that switch, every merge to -`main` still shows up in the portal's **Versions** tab as a new version to check, which -is the `UNREAD_ASSET_REFERENCED` / "Validation didn't finish" hold #437 exists to clear. +tag"). For this plugin it is `release`, set at the submission of 2026-10-04, after +`release` existed and was checked to hold only the slim tree (the order claude-remember +used too). A merge to `main` does not reach the directory; only a tag does. ## Contacting Anthropic about a plugin diff --git a/tests/test-no-paren-case-patterns.sh b/tests/test-no-paren-case-patterns.sh new file mode 100755 index 00000000..326d5ae2 --- /dev/null +++ b/tests/test-no-paren-case-patterns.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# The Anthropic directory validator holds COMMAND_SCRIPT_NOT_FOLLOWED on a case pattern +# written with its optional leading parenthesis, `case "$-" in (*x*)`. Found on +# claude-remember (2026-10-05, release-preview @ c108980): every one of its 121 case +# patterns rewritten without the `(` cleared the hooks that held. Recorded as trigger 11 +# in the shared directory-publishing notes. jit-context ships no such pattern today; this +# suite keeps it that way. +# +# An awk rule opening on a parenthesised condition, `(FILENAME in isgenericfile) {`, +# looks alike and is NOT this shape: v0.16.0 carried three of them and the portal raised +# nothing on them. So the needle wants one pattern word (or `|`-joined words) with no +# space inside the parentheses, and never a `{` after them. Both sides are proven on a +# planted fixture first, because a sweep that finds nothing must be told apart from a +# needle that never matches. +# +# Usage: bash tests/test-no-paren-case-patterns.sh +# +# jit-drive: none -- this suite scans tracked files for a text shape; it defines no assertion helper + +set -uo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +PASS=0 +FAIL=0 + +ok() { + PASS=$((PASS + 1)) + echo " PASS: $1" +} +bad() { + FAIL=$((FAIL + 1)) + echo " FAIL: $1" + shift + [ $# -eq 0 ] || echo " $*" +} + +# A case arm opening on `(`: at the start of a line or after `case ... in`, a `(` not followed by +# a space or another `(` (a subshell or `((...))`), pattern words with no space, closed +# by `)`, and no `{` after it (an awk rule). perl, for the lookaheads. +sweep() { + perl -ne 'next if /^\s*#/; print "$.: $_" if /(^\s*|\bcase\s.*\bin\s+)\((?![\s(])[^\s()]+(?:\s*\|\s*[^\s()]+)*\)(?!\s*\{)/' "$1" +} + +echo "=== controls: the needle matches a parenthesised case arm, and nothing else ===" +fixture="$(mktemp)" +safe="$(mktemp)" +trap 'rm -f "$fixture" "$safe"' EXIT +printf '%s\n' \ + ' case "$-" in (*x*) set +x ;; esac' \ + ' (*.md) echo md ;;' \ + ' (a | b) : ;;' \ + ' (--help|-h)' \ + > "$fixture" +printf '%s\n' \ + '(FILENAME in isgenericfile) {' \ + ' (NR > 1) { next }' \ + ' ( cd "$dir" && pwd )' \ + ' ((i++))' \ + ' *.md) echo md ;;' \ + ' words=(a b c)' \ + ' # a comment may show (*x*)' \ + ' echo "standing in (#417)." >&2' \ + > "$safe" +n=$(sweep "$fixture" | awk 'END { print NR }') +if [ "$n" -eq 4 ]; then + ok "control: all 4 planted parenthesised arms are found" +else + bad "control: found $n of 4 planted arms -- the sweep below is not trustworthy" "$(sweep "$fixture")" +fi +hits="$(sweep "$safe")" +if [ -z "$hits" ]; then + ok "control: awk rules, subshells, arithmetic, arrays, plain arms and comments are left alone" +else + bad "control: a safe shape is flagged" "$hits" +fi + +echo "" +echo "=== no parenthesised case pattern in any shipped script ===" +for f in "$REPO"/scripts/*.sh; do + hits="$(sweep "$f")" + if [ -z "$hits" ]; then + ok "$(basename "$f")" + else + bad "$(basename "$f"): case pattern opening on (" "$hits" + fi +done + +echo "" +echo " $PASS/$((PASS + FAIL)) passed, $FAIL failed" +[ "$FAIL" -eq 0 ]