Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/jit-context/paths/00-manual/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions changelog.d/472.added.paren-case-guard.md
Original file line number Diff line number Diff line change
@@ -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).
64 changes: 34 additions & 30 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <release-commit-sha>`), 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 <release-commit-sha> &&
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.
Expand All @@ -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:
Expand All @@ -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 `[]`).
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
90 changes: 90 additions & 0 deletions tests/test-no-paren-case-patterns.sh
Original file line number Diff line number Diff line change
@@ -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 ]
Loading