Skip to content
Open
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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
{
"name": "claude-code-substrate-memory",
"source": "./",
"description": "Substrate organizational memory for Claude Code.",
"version": "0.1.0"
"description": "Hermes-authoritative hosted Substrate memory for Claude Code.",
"version": "0.2.0"
}
]
}
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "claude-code-substrate-memory",
"version": "0.1.0",
"description": "Substrate organizational memory for Claude Code with MCP tools and fail-open session capture.",
"version": "0.2.0",
"description": "Hermes-authoritative hosted Substrate memory for Claude Code with consent-gated capture.",
"author": {
"name": "Sightline Technologies Inc"
},
Expand Down
50 changes: 46 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,64 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# Runs on the pristine checkout, before any dependency is installed.
- run: python scripts/verify_public_plugin_candidate.py --root .

test:
runs-on: ubuntu-latest
host-contract:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python }}
- run: python -m pip install --upgrade pip
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: python -m pip install -e ".[dev]"
- run: ruff check .
- run: ruff format --check .
- run: python -m compileall -q src scripts
- run: python -m pytest -q
- run: node scripts/plugin_runtime.cjs status
env:
SUBSTRATE_STATE_HOME: ${{ runner.temp }}/substrate-state

full-continuation:
name: full-continuation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: python -m pip install -e ".[dev]"
- name: Run required 50 MiB real-MCP SIGKILL/restart continuation gate
env:
SUBSTRATE_FULL_CONTINUATION: "1"
run: |
set -euo pipefail
python -m pytest -q tests/test_continuation_process.py::test_full_posix_mcp_sigkill_restart_combined_corpus | tee full-continuation.log
grep -F "1 passed" full-continuation.log

protected-publication-gate:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [publication-scan, host-contract, full-continuation]
runs-on: ubuntu-latest
steps:
- name: Require exact protected publication check
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
required="$(gh api "repos/$GITHUB_REPOSITORY/branches/main/protection/required_status_checks" \
--jq '.contexts[]?, .checks[]?.context')"
printf '%s\n' "$required" | grep -Fx 'full-continuation'
225 changes: 225 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
name: release

on:
workflow_dispatch:
inputs:
source_sha:
description: Exact independently reviewed protected-main SHA
required: true
type: string

concurrency:
group: release-v0.2.0
cancel-in-progress: false

permissions:
contents: read

jobs:
platform-gates:
if: github.ref == 'refs/heads/main' && github.sha == inputs.source_sha
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ inputs.source_sha }}
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: python -m pip install -e ".[dev]"
- run: ruff check .
- run: ruff format --check .
- run: python -m compileall -q src scripts
- run: python -m pytest -q
- run: node scripts/plugin_runtime.cjs status
env:
SUBSTRATE_STATE_HOME: ${{ runner.temp }}/substrate-state

full-continuation:
name: full-continuation
if: github.ref == 'refs/heads/main' && github.sha == inputs.source_sha
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ inputs.source_sha }}
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: python -m pip install -e ".[dev]"
- name: Run required 50 MiB real-MCP SIGKILL/restart continuation gate
env:
SUBSTRATE_FULL_CONTINUATION: "1"
run: |
set -euo pipefail
python -m pytest -q tests/test_continuation_process.py::test_full_posix_mcp_sigkill_restart_combined_corpus | tee full-continuation.log
grep -F "1 passed" full-continuation.log

verify:
needs: [platform-gates, full-continuation]
if: github.ref == 'refs/heads/main' && github.sha == inputs.source_sha && needs.platform-gates.result == 'success' && needs.full-continuation.result == 'success'
runs-on: ubuntu-latest
outputs:
artifact_digest: ${{ steps.upload.outputs.artifact-digest }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ inputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Bind exact source to protected main
env:
SOURCE_SHA: ${{ inputs.source_sha }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]
test "$GITHUB_REF" = refs/heads/main
test "$GITHUB_SHA" = "$SOURCE_SHA"
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
git fetch --no-tags origin main
test "$(git rev-parse origin/main)" = "$SOURCE_SHA"
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .protected)" = true
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)" = "$SOURCE_SHA"
required="$(gh api "repos/$GITHUB_REPOSITORY/branches/main/protection/required_status_checks" --jq '.contexts[]?, .checks[]?.context')"
printf '%s\n' "$required" | grep -Fx 'full-continuation'
if git ls-remote --exit-code --tags origin refs/tags/v0.2.0 >/dev/null 2>&1; then
echo 'immutable release tag already exists' >&2
exit 1
fi
- name: Scan pristine candidate
run: python scripts/verify_public_plugin_candidate.py --root .
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- run: python -m pip install -e ".[dev]"
- name: Run complete release checks
run: |
set -euo pipefail
ruff check .
ruff format --check .
python -m compileall -q src scripts
python -m pytest -q
git diff --exit-code
- name: Build exact release bytes twice
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
python scripts/build_release.py --source-commit "$SOURCE_SHA" --output "$RUNNER_TEMP/a"
python scripts/build_release.py --source-commit "$SOURCE_SHA" --output "$RUNNER_TEMP/b"
test "$(find "$RUNNER_TEMP/a" -maxdepth 1 -type f -printf '%f\n' | sort | tr '\n' ' ')" = \
'SHA256SUMS claude_code_substrate_memory.zip install_claude_plugin.py '
diff -r "$RUNNER_TEMP/a" "$RUNNER_TEMP/b"
(cd "$RUNNER_TEMP/a" && sha256sum -c SHA256SUMS)
mkdir dist
cp "$RUNNER_TEMP/a"/* dist/
- id: upload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-${{ inputs.source_sha }}
path: dist/*
if-no-files-found: error
compression-level: 0
retention-days: 1

publish:
needs: [verify, platform-gates, full-continuation]
if: github.ref == 'refs/heads/main' && github.sha == inputs.source_sha && needs.verify.result == 'success' && needs.platform-gates.result == 'success' && needs.full-continuation.result == 'success'
runs-on: ubuntu-latest
environment: public-release
permissions:
actions: read
contents: write
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-${{ inputs.source_sha }}
path: dist
- name: Recheck protected current main after environment approval
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
test "$GITHUB_REF" = refs/heads/main
test "$GITHUB_SHA" = "$SOURCE_SHA"
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .protected)" = true
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)" = "$SOURCE_SHA"
required="$(gh api "repos/$GITHUB_REPOSITORY/branches/main/protection/required_status_checks" --jq '.contexts[]?, .checks[]?.context')"
printf '%s\n' "$required" | grep -Fx 'full-continuation'
checks="$(gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$SOURCE_SHA/check-runs" --jq '.check_runs[] | [.name,.status,.conclusion] | @tsv')"
test "$(printf '%s\n' "$checks" | awk -F '\t' '$1 == "full-continuation" && $2 == "completed" && $3 == "success" { count += 1 } END { print count + 0 }')" -ge 1
for os in ubuntu-latest macos-latest windows-latest; do
for py in 3.11 3.12; do
printf '%s\n' "$checks" | grep -F "platform-gates ($os, $py)" | grep -F $'\tcompleted\tsuccess' >/dev/null
done
done
if git ls-remote --exit-code --tags "https://github.com/$GITHUB_REPOSITORY.git" refs/tags/v0.2.0 >/dev/null 2>&1; then
echo 'immutable release tag already exists' >&2
exit 1
fi
- name: Read back exact closed artifact set
run: |
set -euo pipefail
test -n '${{ needs.verify.outputs.artifact_digest }}'
test "$(find dist -maxdepth 1 -type f -printf '%f\n' | sort | tr '\n' ' ')" = \
'SHA256SUMS claude_code_substrate_memory.zip install_claude_plugin.py '
(cd dist && sha256sum -c SHA256SUMS)
- uses: actions/attest-build-provenance@e3fe62ef559997059fe8380e7d2b4c909e2d65f4 # pinned
with:
subject-path: dist/*
- name: Create immutable tag and release
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
set -euo pipefail
test "$GITHUB_REF" = refs/heads/main
test "$GITHUB_SHA" = "$SOURCE_SHA"
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .protected)" = true
test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)" = "$SOURCE_SHA"
required="$(gh api "repos/$GITHUB_REPOSITORY/branches/main/protection/required_status_checks" --jq '.contexts[]?, .checks[]?.context')"
printf '%s\n' "$required" | grep -Fx 'full-continuation'
if gh release view v0.2.0 --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo 'release already exists; refusing mutation' >&2
exit 1
fi
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref=refs/tags/v0.2.0 -f sha="$SOURCE_SHA" >/dev/null
hashes="$(cat dist/SHA256SUMS)"
gh release create v0.2.0 \
dist/claude_code_substrate_memory.zip \
dist/install_claude_plugin.py \
dist/SHA256SUMS \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
--title 'Claude Code Substrate Memory v0.2.0' \
--notes "Immutable protected-main release. Verify both installer and archive against SHA256SUMS:\n\n$hashes"
mkdir readback
gh release download v0.2.0 --repo "$GITHUB_REPOSITORY" --dir readback
test "$(find readback -maxdepth 1 -type f -printf '%f\n' | sort | tr '\n' ' ')" = \
'SHA256SUMS claude_code_substrate_memory.zip install_claude_plugin.py '
cmp dist/SHA256SUMS readback/SHA256SUMS
cmp dist/claude_code_substrate_memory.zip readback/claude_code_substrate_memory.zip
cmp dist/install_claude_plugin.py readback/install_claude_plugin.py
gh release edit v0.2.0 --repo "$GITHUB_REPOSITORY" --draft=false
11 changes: 4 additions & 7 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
"mcpServers": {
"substrate": {
"type": "stdio",
"command": "python",
"command": "node",
"args": [
"-m",
"claude_code_memory.server"
],
"env": {
"PYTHONPATH": "${CLAUDE_PLUGIN_ROOT}/src"
}
"${CLAUDE_PLUGIN_ROOT}/scripts/plugin_runtime.cjs",
"mcp"
]
}
}
}
2 changes: 1 addition & 1 deletion BOUNDARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This boundary is declared on day one and will not be moved later. Claude Code Su

## Current implementation status

Claude Code Substrate Memory contains the Claude Code plugin/client, local spool and checkpoint machinery, client-side credential redaction, host manifests, and installation metadata. It requires a configured Substrate server for remote memory and delivery.
Claude Code Substrate Memory contains the Claude Code plugin/client, local spool and checkpoint machinery, client-side credential redaction, host manifests, and installation metadata. It connects only to the fixed hosted origin after direct human device approval. Safe Claude history import, server revocation, and a no-server mode are not implemented.

The local runtime, local entity model, privacy-deletion implementation, and policy compiler are on the open side of the permanent boundary but may not be implemented in a given release. The plugin does not authorize agent actions and does not provide a no-server mode unless explicitly documented. Those absences are product gaps, not held commercial features.

Expand Down
18 changes: 14 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
# Changelog

## 0.1.0 — initial public release
## 0.2.0 - release candidate

- Quarantine every contributing record when a recognized credential spans same-lineage record boundaries, including zero-content gaps.
- Bound aggregate cross-record detector state with fail-closed, observable LRU eviction.
- Pair tool results with full, collision-resistant lineage and call identities.
- Adopt Hermes-authoritative fixed-origin RFC 8628 onboarding and secure profile custody.
- Suppress preapproval, sidechain, subagent, background, cron, and worker transcript capture.
- Add strict capture/entity capability gates and canonical-only automatic recall.
- Make spool admission, checkpoints, SessionEnd, retries, auth repair, and status durable/truthful.
- Add uncapped durable SessionEnd jobs, kill/restart recovery, and independent-process checkpoint transactions.
- Add a locked, journaled, content-addressed Claude registration transaction with real rollback, own uninstall, crash recovery, and same-version no-op.
- Terminate the complete POSIX hook process group after bounded grace and use waited Windows `taskkill /T /F` cleanup.
- Add combined large-transcript MCP SIGKILL/restart acceptance plus a CI-scaled cross-platform restart case.
- Retain symlink/TOCTOU rejection, deterministic release artifacts, and protected-main release tooling.
- Match all five Hermes wiki request shapes, including `/wiki/search` and ingest `source_type`.
- Declare safe Claude history import and remote revocation unsupported.

No public release has been published yet.
15 changes: 15 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Compatibility

Release `0.2.0` accepts exactly Claude Code `2.1.237`, Python 3.11/3.12, and Node.js 20/22.
The generated installer refuses a different Claude Code version. CI runs launcher and unit tests
on Linux, macOS, and Windows; a real installed Claude host acceptance run is a human release gate.

Hooks have a 15-second bound. Capture hooks use a small local admission budget and persist a resumable job; no detached worker,
OAuth, credential-helper delivery path, or network delivery consumes that host budget. MCP owns
continuation and delivery. Capture requests use at most 6
seconds and drain uses at most 8 seconds per worker attempt. Automatic recall uses 2.5 seconds total
and 0.9 seconds per request. Device onboarding starts only from explicit `/substrate-setup`, is
resumable for at most 900 seconds, and uses 60-second OAuth/capability requests.

Updates are manual, immutable, checksum-verified atomic swaps. The prior directory is retained for
rollback. State is not part of the code directory and is preserved. There is no auto-update.
Loading
Loading