From 19f9c67e0a381f6c283b0f07c6f2a3322c379520 Mon Sep 17 00:00:00 2001 From: ijevin <2133499+ijevin@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:57:01 +0800 Subject: [PATCH] fix(release): guard Claude Code integration version Release the pending Claude Code integration as v0.7.6 and add a read-only preflight that blocks core releases while Claude Code changes remain unreleased or its manifests and tag disagree. Fixes #2900 (cherry picked from commit 6ee78b7eab8ffdb0d5666250afd00afb84cadce4) --- .claude-plugin/marketplace.json | 2 +- .../claude-code/.claude-plugin/plugin.json | 2 +- scripts/release-integration.sh | 66 +++++++++- scripts/release.sh | 19 ++- scripts/tests/test-release-integration.sh | 121 ++++++++++++++++++ skills/hindsight-docs/references/openapi.json | 18 ++- 6 files changed, 214 insertions(+), 14 deletions(-) create mode 100755 scripts/tests/test-release-integration.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7769bd5bd8..c246598add 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "hindsight", - "version": "0.7.5", + "version": "0.7.6", "description": "Official Hindsight integrations for Claude Code", "owner": { "name": "vectorize-io" diff --git a/hindsight-integrations/claude-code/.claude-plugin/plugin.json b/hindsight-integrations/claude-code/.claude-plugin/plugin.json index 33b7d5e043..ddf4510deb 100644 --- a/hindsight-integrations/claude-code/.claude-plugin/plugin.json +++ b/hindsight-integrations/claude-code/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hindsight-memory", "description": "Automatic long-term memory for Claude Code via Hindsight. Recalls relevant memories before each prompt, retains conversation transcripts, and provides knowledge page tools.", - "version": "0.7.5", + "version": "0.7.6", "author": {"name": "Hindsight Team", "url": "https://vectorize.io/hindsight"}, "license": "MIT", "keywords": ["memory", "hindsight", "recall", "retain"] diff --git a/scripts/release-integration.sh b/scripts/release-integration.sh index dd754854bc..0ce8ab6026 100755 --- a/scripts/release-integration.sh +++ b/scripts/release-integration.sh @@ -16,10 +16,11 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1"; } VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "aider" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "coding-agents" "codex" "composio" "continue" "copilot-cli" "crewai" "cursor" "cursor-cli" "devin-desktop" "dify" "eve" "flowise" "gemini-spark" "github-copilot" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "openhands" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi" "zcode" "zed") usage() { - print_error "Usage: $0 " + print_error "Usage: $0 [--check] [version]" echo "" echo " integration One of: ${VALID_INTEGRATIONS[*]}" echo " version Semantic version (e.g. 0.2.0) or bump keyword: patch, minor, major" + echo " --check Verify the integration has no changes since its latest release tag" echo "" echo "Examples:" echo " $0 litellm 0.2.0" @@ -28,12 +29,21 @@ usage() { exit 1 } -if [ -z "$1" ] || [ -z "$2" ]; then +CHECK_ONLY=false +if [ "${1:-}" = "--check" ]; then + CHECK_ONLY=true + shift +fi + +if [ -z "${1:-}" ] || { [ "$CHECK_ONLY" = "false" ] && [ -z "${2:-}" ]; }; then + usage +fi +if [ "$CHECK_ONLY" = "true" ] && [ "$#" -ne 1 ]; then usage fi INTEGRATION=$1 -VERSION_ARG=$2 +VERSION_ARG=${2:-} # Validate integration name VALID=false @@ -65,6 +75,56 @@ get_current_version() { fi } +check_integration_release() { + local dir="hindsight-integrations/$INTEGRATION" + local current_version last_tag tag_version unreleased_count + + if [ ! -d "$dir" ]; then + print_error "Integration directory not found: $dir" + exit 1 + fi + + current_version=$(get_current_version) + if [ -z "$current_version" ]; then + print_error "Could not read current version for '$INTEGRATION'" + exit 1 + fi + + if [ "$INTEGRATION" = "claude-code" ]; then + local marketplace_version + marketplace_version=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["version"])' .claude-plugin/marketplace.json) + if [ "$current_version" != "$marketplace_version" ]; then + print_error "Claude Code plugin and marketplace versions must match ($current_version != $marketplace_version)" + exit 1 + fi + fi + + last_tag=$(git tag --list "integrations/$INTEGRATION/v*" --sort=-v:refname | head -1) + if [ -z "$last_tag" ]; then + print_error "$INTEGRATION has no integration release tag" + exit 1 + fi + + unreleased_count=$(git rev-list --count "$last_tag"..HEAD -- "$dir") + if [ "$unreleased_count" -ne 0 ]; then + print_error "$INTEGRATION has $unreleased_count unreleased changes since $last_tag; run scripts/release-integration.sh $INTEGRATION before the core release" + exit 1 + fi + + tag_version=${last_tag#integrations/$INTEGRATION/v} + if [ "$current_version" != "$tag_version" ]; then + print_error "$INTEGRATION manifest version $current_version does not match latest release tag $last_tag" + exit 1 + fi + + print_info "$INTEGRATION is released at v$current_version" +} + +if [ "$CHECK_ONLY" = "true" ]; then + check_integration_release + exit 0 +fi + # Bump a semver component: bump_version bump_version() { local current=$1 part=$2 diff --git a/scripts/release.sh b/scripts/release.sh index e1bc78b6bc..9d80624aa0 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -56,6 +56,11 @@ if [[ -n $(git status -s) ]]; then exit 1 fi +# Integrations keep independent versions, but a core release must not ship +# unreleased Claude Code changes behind a stale marketplace manifest. +print_info "Checking Claude Code integration release status..." +./scripts/release-integration.sh --check claude-code + # Check if tag already exists if git rev-parse "v$VERSION" >/dev/null 2>&1; then print_error "Tag v$VERSION already exists" @@ -199,13 +204,13 @@ else print_warn "update-docs-version.sh not found, skipping docs update" fi -# Regenerate OpenAPI spec and clients with new version -print_info "Regenerating OpenAPI spec and client SDKs..." -if ./scripts/generate-openapi.sh && ./scripts/generate-clients.sh; then - print_info "✓ OpenAPI spec and clients regenerated" +# Regenerate OpenAPI spec, clients, and the docs skill copy with new version +print_info "Regenerating OpenAPI spec, client SDKs, and docs skill..." +if ./scripts/generate-openapi.sh && ./scripts/generate-clients.sh && ./scripts/generate-docs-skill.sh; then + print_info "✓ OpenAPI spec, clients, and docs skill regenerated" else - print_error "Failed to regenerate clients" - print_warn "You may need to fix this manually before committing" + print_error "Failed to regenerate OpenAPI, clients, or docs skill" + print_warn "Fix the generation failure before committing" read -p "Continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then @@ -227,7 +232,7 @@ PATCH_VERSION=$(echo "$VERSION" | sed -E 's/^[0-9]+\.[0-9]+\.([0-9]+)$/\1/') COMMIT_MSG="Release v$VERSION - Update version to $VERSION in all components -- Regenerate OpenAPI spec and client SDKs +- Regenerate OpenAPI spec, client SDKs, and docs skill - Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-embed - Python client: hindsight-clients/python - TypeScript client: hindsight-clients/typescript diff --git a/scripts/tests/test-release-integration.sh b/scripts/tests/test-release-integration.sh new file mode 100755 index 0000000000..0323482856 --- /dev/null +++ b/scripts/tests/test-release-integration.sh @@ -0,0 +1,121 @@ +#!/bin/bash +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "FAIL: $1" >&2 + exit 1 +} + +write_manifest() { + local path=$1 version=$2 + mkdir -p "$(dirname "$path")" + printf '{\n "version": "%s"\n}\n' "$version" > "$path" +} + +# Exercise the real integration release script in an isolated repository. The +# check path must not create tags, push, or require changelog credentials. +INTEGRATION_REPO="$TMP_DIR/integration" +mkdir -p "$INTEGRATION_REPO/scripts" "$INTEGRATION_REPO/hindsight-integrations/claude-code" +cp "$ROOT/scripts/release-integration.sh" "$INTEGRATION_REPO/scripts/release-integration.sh" +write_manifest "$INTEGRATION_REPO/hindsight-integrations/claude-code/.claude-plugin/plugin.json" "0.7.5" +write_manifest "$INTEGRATION_REPO/.claude-plugin/marketplace.json" "0.7.5" +printf 'initial\n' > "$INTEGRATION_REPO/hindsight-integrations/claude-code/integration.txt" + +git -C "$INTEGRATION_REPO" init -q -b main +git -C "$INTEGRATION_REPO" config user.name "Release Test" +git -C "$INTEGRATION_REPO" config user.email "release-test@example.com" +git -C "$INTEGRATION_REPO" add . +git -C "$INTEGRATION_REPO" commit -q -m "initial integration" +git -C "$INTEGRATION_REPO" tag "integrations/claude-code/v0.7.5" +printf 'changed\n' >> "$INTEGRATION_REPO/hindsight-integrations/claude-code/integration.txt" +git -C "$INTEGRATION_REPO" add . +git -C "$INTEGRATION_REPO" commit -q -m "change integration" + +set +e +STALE_OUTPUT=$(cd "$INTEGRATION_REPO" && ./scripts/release-integration.sh --check claude-code 2>&1) +STALE_STATUS=$? +set -e +[ "$STALE_STATUS" -ne 0 ] || fail "stale Claude Code manifest passed the release check" +case "$STALE_OUTPUT" in + *"unreleased changes"*) ;; + *) fail "stale release check did not explain the unreleased changes: $STALE_OUTPUT" ;; +esac + +write_manifest "$INTEGRATION_REPO/hindsight-integrations/claude-code/.claude-plugin/plugin.json" "0.7.6" +write_manifest "$INTEGRATION_REPO/.claude-plugin/marketplace.json" "0.7.6" +git -C "$INTEGRATION_REPO" add . +git -C "$INTEGRATION_REPO" commit -q -m "release claude-code 0.7.6" +git -C "$INTEGRATION_REPO" tag "integrations/claude-code/v0.7.6" +(cd "$INTEGRATION_REPO" && ./scripts/release-integration.sh --check claude-code) + +# Nested plugin entries may carry their own versions. The guard must read the +# top-level marketplace version regardless of JSON key order. +cat > "$INTEGRATION_REPO/.claude-plugin/marketplace.json" <<'JSON' +{ + "plugins": [{"name": "hindsight-memory", "version": "9.9.9"}], + "version": "0.7.6" +} +JSON +(cd "$INTEGRATION_REPO" && ./scripts/release-integration.sh --check claude-code) + +write_manifest "$INTEGRATION_REPO/.claude-plugin/marketplace.json" "0.8.4" +set +e +MISMATCH_OUTPUT=$(cd "$INTEGRATION_REPO" && ./scripts/release-integration.sh --check claude-code 2>&1) +MISMATCH_STATUS=$? +set -e +[ "$MISMATCH_STATUS" -ne 0 ] || fail "mismatched Claude Code manifests passed the release check" +case "$MISMATCH_OUTPUT" in + *"must match"*) ;; + *) fail "manifest mismatch did not produce an actionable error: $MISMATCH_OUTPUT" ;; +esac + +set +e +EXTRA_ARG_OUTPUT=$(cd "$INTEGRATION_REPO" && ./scripts/release-integration.sh --check claude-code 0.9.9 2>&1) +EXTRA_ARG_STATUS=$? +set -e +[ "$EXTRA_ARG_STATUS" -ne 0 ] || fail "--check silently accepted an unexpected version argument" +case "$EXTRA_ARG_OUTPUT" in + *"Usage:"*) ;; + *) fail "extra --check argument did not show usage: $EXTRA_ARG_OUTPUT" ;; +esac + +# The core release path must invoke the independent integration guard before it +# mutates files or reaches any tag/push/changelog work. +CORE_REPO="$TMP_DIR/core" +mkdir -p "$CORE_REPO/scripts" +cp "$ROOT/scripts/release.sh" "$CORE_REPO/scripts/release.sh" +printf '#!/bin/bash\nprintf "%%s\\n" "$*" > "$CHECK_LOG"\nexit 42\n' > "$CORE_REPO/scripts/release-integration.sh" +chmod +x "$CORE_REPO/scripts/release-integration.sh" +git -C "$CORE_REPO" init -q -b main +git -C "$CORE_REPO" config user.name "Release Test" +git -C "$CORE_REPO" config user.email "release-test@example.com" +printf 'fixture\n' > "$CORE_REPO/README.md" +git -C "$CORE_REPO" add . +git -C "$CORE_REPO" commit -q -m "core fixture" + +CHECK_LOG="$TMP_DIR/check-args" +set +e +CORE_OUTPUT=$(cd "$CORE_REPO" && CHECK_LOG="$CHECK_LOG" ./scripts/release.sh 9.9.9 &1) +CORE_STATUS=$? +set -e +[ "$CORE_STATUS" -eq 42 ] || fail "core release bypassed the integration guard (status $CORE_STATUS): $CORE_OUTPUT" +[ "$(cat "$CHECK_LOG")" = "--check claude-code" ] || fail "core release called the guard with unexpected arguments" +[ ! -e "$CORE_REPO/.git/refs/tags/v9.9.9" ] || fail "core release created a tag after the guard failed" + +# Core releases must refresh the generated docs skill after regenerating the +# OpenAPI spec and clients so committed copies cannot drift between releases. +grep -q 'generate-openapi.sh.*generate-clients.sh.*generate-docs-skill.sh' "$ROOT/scripts/release.sh" || \ + fail "core release does not regenerate the docs skill after OpenAPI and clients" + +PLUGIN_VERSION=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["version"])' \ + "$ROOT/hindsight-integrations/claude-code/.claude-plugin/plugin.json") +MARKETPLACE_VERSION=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["version"])' \ + "$ROOT/.claude-plugin/marketplace.json") +[ "$PLUGIN_VERSION" = "0.7.6" ] || fail "plugin manifest version is $PLUGIN_VERSION, expected 0.7.6" +[ "$MARKETPLACE_VERSION" = "0.7.6" ] || fail "marketplace version is $MARKETPLACE_VERSION, expected 0.7.6" + +echo "PASS: Claude Code release guard regression tests" diff --git a/skills/hindsight-docs/references/openapi.json b/skills/hindsight-docs/references/openapi.json index 4a3cf6aef4..28b83303cb 100644 --- a/skills/hindsight-docs/references/openapi.json +++ b/skills/hindsight-docs/references/openapi.json @@ -3026,7 +3026,7 @@ "Documents" ], "summary": "List documents", - "description": "List documents with pagination and optional search. Documents are the source content from which memory units are extracted.", + "description": "List documents with pagination and optional search, most recently written first (`updated_at` descending). Documents are the source content from which memory units are extracted.", "operationId": "list_documents", "parameters": [ { @@ -6938,7 +6938,20 @@ "type": "null" } ], - "title": "Last Document At" + "title": "Last Document At", + "description": "When a document was last *ingested* into this bank. Appending to an existing document does not move this \u2014 use `last_write_at` for write activity." + }, + "last_write_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Write At", + "description": "When anything was last written to this bank: a document retained (including appends to an existing document) or a fact stored. Null if the bank is empty." } }, "type": "object", @@ -6977,6 +6990,7 @@ }, "fact_count": 156, "last_document_at": "2024-01-16T14:20:00Z", + "last_write_at": "2024-01-17T09:05:00Z", "mission": "I am a software engineer helping my team ship quality code", "name": "Alice", "updated_at": "2024-01-16T14:20:00Z"