-
Notifications
You must be signed in to change notification settings - Fork 0
Establish the initial 0.1.0 release #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", | ||
| "name": "naoto256-amtr", | ||
| "description": "Amnestic Trace — ephemeral replacement memory across a context boundary.", | ||
| "owner": { | ||
| "name": "Naoto Morishima" | ||
| }, | ||
| "plugins": [ | ||
| { | ||
| "name": "amtr", | ||
| "description": "Replaces a session's short-term working memory across compaction, and hands it to another session on request. A PreCompact hook starts a detached extraction over the journal since the last compaction, so it runs beside the compaction rather than after it. Three hooks then race to inject the result — as the compaction ends, at the next tool call, or at the next prompt — whichever comes first once it is ready. Adds the /amtr skill for cross-session handoff.", | ||
| "author": { | ||
| "name": "Naoto Morishima" | ||
| }, | ||
| "source": "./plugin" | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, 'release/*'] | ||
| pull_request: | ||
| branches: [main, 'release/*'] | ||
| workflow_dispatch: | ||
|
|
||
| # Minimum required for `actions/checkout` (read the repo). Cache via | ||
| # `Swatinem/rust-cache` works in read-only fallback without an explicit | ||
| # `actions: write` grant. No GitHub API writes from this job. | ||
| # | ||
| # Cache saves are gated on non-PR events (see `save-if:` below). PR runs can | ||
| # restore an existing cache but cannot write one back, so a hostile PR cannot | ||
| # poison the cache a subsequent trusted `push` run would restore. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| CARGO_TERM_COLOR: always | ||
|
|
||
| jobs: | ||
| msrv: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install minimum supported Rust | ||
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin-audit:2026-05-21 29eef33 | stable | ||
| with: | ||
| toolchain: 1.88.0 | ||
|
|
||
| - name: Check MSRV | ||
| run: cargo check --locked | ||
|
|
||
| check: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin-audit:2026-05-21 29eef33 | stable | ||
| with: | ||
| components: clippy, rustfmt | ||
|
|
||
| - name: Cache cargo | ||
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # pin-audit:2026-05-21 e18b497 | v2 | ||
| with: | ||
| save-if: ${{ github.event_name != 'pull_request' }} | ||
|
|
||
| - name: Rustfmt | ||
| run: cargo fmt --all -- --check | ||
|
|
||
| - name: Build | ||
| run: cargo build --locked | ||
|
|
||
| - name: Test | ||
| run: cargo test --locked | ||
|
|
||
| - name: Clippy | ||
| run: cargo clippy --locked --all-targets -- -D warnings | ||
|
|
||
| # The hook script is the other half of this tool and is not covered by | ||
| # `cargo test`: it is what the host actually executes, and a failure in it | ||
| # fails open and silently, which is precisely the failure this project cannot | ||
| # detect at runtime by design. | ||
| # | ||
| # Syntax checking alone is not enough. A redirection error on a POSIX special | ||
| # built-in terminates the shell outright — dash does this, bash and zsh do | ||
| # not — so a hook can parse cleanly everywhere and still die on the first | ||
| # line of real work on Debian and Ubuntu. Only running it catches that, which | ||
| # is what the regression script does under all four supported shells. | ||
| hook-script: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install ksh | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install -y ksh | ||
|
|
||
| - name: Shell syntax | ||
| run: | | ||
| sh -n plugin/tools/amtr-hook.sh | ||
| dash -n plugin/tools/amtr-hook.sh | ||
|
|
||
| - name: Hook behaviour | ||
| run: tests/hook-regressions.sh | ||
|
|
||
|
|
||
| # `cargo deny` gates every dimension deny.toml covers — RustSec advisories, | ||
| # duplicate and wildcard dependencies, source restrictions, license policy — | ||
| # against Cargo.lock. Pinned and `--locked` so the gate is itself | ||
| # reproducible: a floating tool version could silently change what "pass" | ||
| # means. | ||
| supply-chain: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin-audit:2026-05-21 29eef33 | stable | ||
|
|
||
| - name: Cache cargo | ||
| uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # pin-audit:2026-05-21 e18b497 | v2 | ||
| with: | ||
| key: supply-chain | ||
| save-if: ${{ github.event_name != 'pull_request' }} | ||
|
|
||
| - name: Install cargo-deny | ||
| run: cargo install --locked --version 0.18.6 cargo-deny | ||
|
|
||
| - name: cargo deny check | ||
| run: cargo deny --locked check advisories bans sources licenses | ||
|
|
||
| # The manifests decide whether either host loads anything at all. They are | ||
| # data, so nothing else in this pipeline would notice a trailing comma or a | ||
| # path that no longer resolves. | ||
| manifests: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: JSON is well-formed | ||
| run: | | ||
| for f in .claude-plugin/marketplace.json \ | ||
| plugin/.claude-plugin/plugin.json \ | ||
| plugin/.codex-plugin/plugin.json \ | ||
| plugin/hooks/claude.json \ | ||
| plugin/hooks/codex.json; do | ||
| python3 -m json.tool "$f" > /dev/null | ||
| done | ||
|
|
||
| # Each manifest names its hook file explicitly; neither host falls back | ||
| # to a convention, so a rename that missed one would silently disable | ||
| # that host. | ||
| - name: Declared hook files exist | ||
| run: | | ||
| for manifest in plugin/.claude-plugin/plugin.json plugin/.codex-plugin/plugin.json; do | ||
| rel=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['hooks'])" "$manifest") | ||
| test -f "plugin/${rel#./}" || { echo "missing: $manifest -> $rel"; exit 1; } | ||
| done | ||
|
|
||
| # Hook declarations are the only source of these canonical arguments. | ||
| # The shell regressions can prove the adapter forwards each form, but | ||
| # cannot prove either host manifest actually names it. | ||
| - name: Canonical hook invocations are declared | ||
| run: | | ||
| for f in plugin/hooks/claude.json plugin/hooks/codex.json; do | ||
| python3 - "$f" <<'PY' | ||
| import json, sys | ||
|
|
||
| path = sys.argv[1] | ||
| declared = json.load(open(path))["hooks"] | ||
| expected = { | ||
| "PreCompact": ("", " synthesize"), | ||
| "SessionStart": ("compact", " recall SessionStart"), | ||
| "PreToolUse": (None, " recall PreToolUse"), | ||
| "UserPromptSubmit": ("", " recall UserPromptSubmit"), | ||
| } | ||
| unexpected = set(declared) - set(expected) | ||
| if unexpected: | ||
| sys.exit(f"{path}: unexpected hook events declared: {sorted(unexpected)}") | ||
| for event, (matcher, suffix) in expected.items(): | ||
| entries = declared.get(event, []) | ||
| if len(entries) != 1 or len(entries[0].get("hooks", [])) != 1: | ||
| sys.exit(f"{path}: expected exactly one {event} command") | ||
| if matcher is not None and entries[0].get("matcher") != matcher: | ||
| sys.exit(f"{path}: {event} matcher is {entries[0].get('matcher')!r}, not {matcher!r}") | ||
| command = entries[0]["hooks"][0]["command"] | ||
| if "tools/amtr-hook.sh" not in command: | ||
| sys.exit(f"{path}: {event} command does not invoke the adapter: {command!r}") | ||
| if not command.endswith(suffix): | ||
| sys.exit(f"{path}: {event} command does not end in {suffix!r}: {command!r}") | ||
| PY | ||
| done | ||
|
|
||
| # The version appears in three places and they are read by three | ||
| # different consumers; a release tagged against a stale one installs the | ||
| # wrong thing. | ||
| - name: Versions agree | ||
| run: | | ||
| crate=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2) | ||
| for manifest in plugin/.claude-plugin/plugin.json plugin/.codex-plugin/plugin.json; do | ||
| got=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['version'])" "$manifest") | ||
| test "$got" = "$crate" || { echo "$manifest is $got, Cargo.toml is $crate"; exit 1; } | ||
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - 'v*' | ||
|
|
||
| # No workflow-level grant: only the job that creates the release needs write, | ||
| # and the build jobs run first with a token that cannot publish anything. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| CARGO_TERM_COLOR: always | ||
|
|
||
| jobs: | ||
| # Both platforms, because nothing about the tool is platform-specific and a | ||
| # host runs wherever the user is. | ||
| build: | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| - runner: macos-latest | ||
| target: aarch64-apple-darwin | ||
| - runner: ubuntu-latest | ||
| target: x86_64-unknown-linux-gnu | ||
| runs-on: ${{ matrix.runner }} | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Validate release version | ||
| env: | ||
| RELEASE_TAG: ${{ github.ref_name }} | ||
| run: | | ||
| python3 - <<'PY' | ||
| import json | ||
| import os | ||
| import re | ||
| import tomllib | ||
| from pathlib import Path | ||
|
|
||
| tag = os.environ["RELEASE_TAG"] | ||
| match = re.fullmatch(r"v(\d+\.\d+\.\d+)", tag) | ||
| if match is None: | ||
| raise SystemExit(f"release tag must be vMAJOR.MINOR.PATCH, got {tag!r}") | ||
| expected = match.group(1) | ||
|
|
||
| cargo = tomllib.loads(Path("Cargo.toml").read_text()) | ||
| lock = tomllib.loads(Path("Cargo.lock").read_text()) | ||
| versions = {"Cargo.toml": cargo["package"]["version"]} | ||
| root = [ | ||
| package for package in lock["package"] | ||
| if package["name"] == cargo["package"]["name"] and "source" not in package | ||
| ] | ||
| if len(root) != 1: | ||
| raise SystemExit(f"Cargo.lock has {len(root)} root amtr packages, expected one") | ||
| versions["Cargo.lock"] = root[0]["version"] | ||
| for manifest in ( | ||
| "plugin/.claude-plugin/plugin.json", | ||
| "plugin/.codex-plugin/plugin.json", | ||
| ): | ||
| versions[manifest] = json.loads(Path(manifest).read_text())["version"] | ||
|
|
||
| mismatches = {path: version for path, version in versions.items() if version != expected} | ||
| if mismatches: | ||
| detail = ", ".join(f"{path}={version}" for path, version in mismatches.items()) | ||
| raise SystemExit(f"{tag} disagrees with product metadata: {detail}") | ||
|
|
||
| changelog = Path("CHANGELOG.md").read_text() | ||
| heading = rf"^## \[{re.escape(expected)}\](?: - \d{{4}}-\d{{2}}-\d{{2}})?$" | ||
| if re.search(heading, changelog, re.MULTILINE) is None: | ||
| raise SystemExit(f"CHANGELOG.md has no exact [{expected}] release heading") | ||
| PY | ||
|
|
||
| - name: Install Rust | ||
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # pin-audit:2026-05-21 29eef33 | stable | ||
| with: | ||
| targets: ${{ matrix.target }} | ||
|
|
||
| - name: Build release | ||
| run: cargo build --release --locked --target ${{ matrix.target }} | ||
|
|
||
| - name: Package | ||
| run: | | ||
| tar -czf "amtr-${GITHUB_REF_NAME}-${{ matrix.target }}.tar.gz" \ | ||
| -C "target/${{ matrix.target }}/release" amtr | ||
|
|
||
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # pin-audit:2026-07-11 043fb46 | v7.0.1 | ||
| with: | ||
| name: amtr-${{ matrix.target }} | ||
| path: amtr-*.tar.gz | ||
| if-no-files-found: error | ||
|
|
||
| release: | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| # The only job that publishes anything. | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # pin-audit:2026-06-03 de0fac2 | v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # pin-audit:2026-07-11 3e5f45b | v8.0.1 | ||
| with: | ||
| path: dist | ||
| merge-multiple: true | ||
|
|
||
| - name: Extract release notes from CHANGELOG | ||
| id: notes | ||
| run: | | ||
| VERSION="${GITHUB_REF_NAME#v}" | ||
| # Body: everything between this version's heading and the next. | ||
| awk -v ver="$VERSION" ' | ||
| $0 ~ "^## \\["ver"\\]" { found=1; next } | ||
| found && /^## \[/ { exit } | ||
| found { print } | ||
| ' CHANGELOG.md > RELEASE_NOTES.md | ||
| if [ ! -s RELEASE_NOTES.md ]; then | ||
| echo "::warning::No CHANGELOG entry found for ${VERSION}; release body will be empty." | ||
| fi | ||
| # Tagline (optional): a `> ...` blockquote line immediately following | ||
| # the version heading, used as the second clause of the release name. | ||
| TAGLINE=$(awk -v ver="$VERSION" ' | ||
| $0 ~ "^## \\["ver"\\]" { in_section=1; next } | ||
| in_section && /^>/ { sub(/^> ?/, ""); print; exit } | ||
| in_section && /^##/ { exit } | ||
| ' CHANGELOG.md) | ||
| if [ -n "$TAGLINE" ]; then | ||
| NAME="amtr ${VERSION} — ${TAGLINE}" | ||
| else | ||
| NAME="amtr ${VERSION}" | ||
| fi | ||
| echo "name=${NAME}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Generate checksums | ||
| # Lets consumers verify offline (`sha256sum -c SHA256SUMS`). Basenames | ||
| # rather than paths, so the check works from the download directory. | ||
| run: | | ||
| cd dist | ||
| sha256sum *.tar.gz > SHA256SUMS | ||
|
|
||
| - name: Upload to GitHub Release | ||
| env: | ||
| # Pre-installed gh CLI authenticates with the job's GITHUB_TOKEN; no | ||
| # third-party action in the trust path. | ||
| GH_TOKEN: ${{ github.token }} | ||
| # Passed as an env var rather than interpolated into `run:`. The value | ||
| # comes from CHANGELOG via awk, so it is repo-controlled, but | ||
| # expression substitution into a shell command is the wrong shape to | ||
| # rely on if that ever stops being true. | ||
| RELEASE_TITLE: ${{ steps.notes.outputs.name }} | ||
| run: | | ||
| gh release create "${GITHUB_REF_NAME}" \ | ||
| dist/*.tar.gz dist/SHA256SUMS \ | ||
| --title "$RELEASE_TITLE" \ | ||
| --notes-file RELEASE_NOTES.md | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.