Skip to content

Commit 4da404c

Browse files
dzmitrys-devclaude
andcommitted
release: v0.1.1 — CI fix, AGENTS.md/CLAUDE.md, update-check, PyPI publish
Hardens v0.1.0 with four scope additions ahead of the SoftChat rip-out (Plan 80.6-14): 1. CI fix: test_cli_smoke subprocess pins NO_COLOR=1, TERM=dumb, COLUMNS=200, pops FORCE_COLOR — eliminates Rich color-escape pollution that broke help-text assertions on GH Actions runners. 2. AGENTS.md + CLAUDE.md added per the Apr 2026 cross-tool convention. Concise, dense imperative bullets; CLAUDE.md delegates to AGENTS.md. 3. update_check module: pip-style fire-and-forget GitHub Releases probe. Daemon thread writes platformdirs.user_cache_dir/update_check.json; next invocation prints stderr footer if newer release cached. 24h TTL, 6h backoff on 403/429, ETag-aware. Suppress with SUPAMEM_NO_UPDATE_CHECK, NO_UPDATE_NOTIFIER, or CI. Skipped when stderr is non-TTY. Surfaced in supamem doctor. 4. Release pipeline: split into build / publish-pypi / github-release jobs. PyPI publishing via Trusted Publisher OIDC (environment: pypi, id-token: write); first publish auto-converts the pending publisher registered at pypi.org. License metadata migrated to PEP 639 SPDX expression. New deps: platformdirs>=4.2, packaging>=23.0. 180/180 tests green; ruff clean; twine check PASSED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 335d7ab commit 4da404c

11 files changed

Lines changed: 876 additions & 14 deletions

File tree

.github/workflows/release.yml

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ on:
55
tags: ['v*']
66

77
jobs:
8-
release:
8+
build:
9+
name: Build wheel + sdist
910
runs-on: ubuntu-latest
10-
permissions:
11-
contents: write
1211
steps:
1312
- uses: actions/checkout@v4
1413
with:
@@ -19,15 +18,56 @@ jobs:
1918
with:
2019
python-version: "3.12"
2120

22-
- name: Install build
23-
run: pip install build
21+
- name: Install build + twine
22+
run: pip install build twine
2423

25-
- name: Build wheel + sdist
24+
- name: Build
2625
run: python -m build
2726

27+
- name: Twine check
28+
run: twine check dist/*
29+
2830
- name: List artifacts
2931
run: ls -lh dist/
3032

33+
- name: Upload dist artifact
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: dist
37+
path: dist/
38+
39+
publish-pypi:
40+
name: Publish to PyPI (Trusted Publisher OIDC)
41+
needs: build
42+
runs-on: ubuntu-latest
43+
environment:
44+
name: pypi
45+
url: https://pypi.org/p/supamem
46+
permissions:
47+
id-token: write
48+
steps:
49+
- name: Download dist artifact
50+
uses: actions/download-artifact@v4
51+
with:
52+
name: dist
53+
path: dist/
54+
55+
- name: Publish to PyPI
56+
uses: pypa/gh-action-pypi-publish@release/v1
57+
58+
github-release:
59+
name: Create GitHub release
60+
needs: build
61+
runs-on: ubuntu-latest
62+
permissions:
63+
contents: write
64+
steps:
65+
- name: Download dist artifact
66+
uses: actions/download-artifact@v4
67+
with:
68+
name: dist
69+
path: dist/
70+
3171
- name: Create GitHub release
3272
uses: softprops/action-gh-release@v2
3373
with:

AGENTS.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# AGENTS.md — supamem
2+
3+
Operational guide for AI coding agents working in this repository. Project-agnostic dual-memory CLI for Claude Code, Cursor, and OpenCode.
4+
5+
## Project Snapshot
6+
7+
- **Language:** Python 3.12+
8+
- **Build backend:** hatchling
9+
- **Package manager:** `uv`
10+
- **CLI entry:** `supamem``src/supamem/cli.py:main`
11+
- **Tests:** `pytest` (config in `pyproject.toml`)
12+
- **Lint/format:** `ruff` (line length 100, target py312)
13+
- **External deps:** Qdrant 1.10+ (HTTP), MCP 1.13+, fastembed, langchain-text-splitters
14+
15+
## Architecture
16+
17+
```
18+
src/supamem/
19+
├── cli.py # Typer-based CLI dispatcher
20+
├── console.py # Shared Rich console + theme (single source of branding)
21+
├── config.py # Pydantic config schema (D-38)
22+
├── config_io.py # Discovery: project → user → defaults; merge order locked
23+
├── doctor.py # Health + drift report (Plan 80.6-11)
24+
├── init.py # Greenfield bootstrap (Plan 80.6-08)
25+
├── migrate.py # Brownfield migration paths (Plan 80.6-09)
26+
├── mcp_server.py # MCP server entrypoint
27+
├── embedders/ # minilm, bm25 — pluggable via entry-points
28+
├── indexer/ # chunker (markdown_header), manifest
29+
├── retrieval/ # tuned_hybrid, dense, bm25 backends
30+
├── eval/ # Bench runner + bundled goldens (Plan 80.6-12)
31+
├── hooks/ # claude_code, cursor — per-client snapshot hooks
32+
├── install/ # claude_code, cursor, opencode — config installers
33+
├── share/ # Canonical artifact templates
34+
└── stats/ # Welford counter for usage telemetry
35+
```
36+
37+
## Plugin Entry Points (D-48)
38+
39+
Three plugin groups in `pyproject.toml`. Third parties add backends without forking:
40+
41+
- `supamem.retrieval``tuned_hybrid`, `dense`, `bm25`
42+
- `supamem.embedder``minilm`, `bm25`
43+
- `supamem.chunker``markdown_header`
44+
45+
## Config Discovery (D-38)
46+
47+
Order: `./.supamem/config.toml` (project) → `~/.config/supamem/config.toml` (user) → `share/default.toml` (shipped). Merge is shallow; project wins.
48+
49+
## Hard Constraints
50+
51+
- NEVER bypass failing tests — fix root cause
52+
- NEVER hardcode collection names; always read from `config.collection`
53+
- NEVER print to stdout in MCP server — JSON-RPC contract requires stdio purity (use `err_console`)
54+
- NEVER write to `~/` outside `~/.cache/supamem/` and `~/.config/supamem/` without explicit user opt-in
55+
- NEVER delete a Qdrant collection without `--force` flag confirmation
56+
- ALWAYS use `console.py` exports for terminal output (no bare `print()`)
57+
- ALWAYS run `pytest` from project root via `uv run pytest`
58+
59+
## Workflow
60+
61+
```bash
62+
# Setup
63+
uv sync --extra dev
64+
65+
# Tests
66+
uv run pytest # full suite
67+
uv run pytest tests/test_X.py -v # single file
68+
69+
# Lint / type
70+
uv run ruff check src tests
71+
uv run ruff format src tests
72+
73+
# Build + verify
74+
uv build
75+
uvx twine check dist/*
76+
77+
# Run locally
78+
uv run supamem --help
79+
uv run python -m supamem doctor
80+
```
81+
82+
## Test Discipline
83+
84+
- Subprocess-based CLI smoke tests (`test_cli_smoke.py`) MUST pin a deterministic env: `NO_COLOR=1`, `TERM=dumb`, `COLUMNS=200` — pop `FORCE_COLOR`. Rich autodetect alone is insufficient on CI runners.
85+
- Use `pytest-asyncio` with `mode=Mode.STRICT`; mark async tests with `@pytest.mark.asyncio`.
86+
- Mock Qdrant via fixtures, not by hitting a live instance — bench/eval suites are the integration boundary.
87+
88+
## Release Process
89+
90+
1. Bump `version` in `pyproject.toml` and add `CHANGELOG.md` entry.
91+
2. Verify license metadata complies with PEP 639 (`license = "MIT"` SPDX, no `License ::` classifier).
92+
3. `uv build && uvx twine check dist/*`
93+
4. Create annotated tag: `git tag -a vX.Y.Z -m "..."`
94+
5. Push tag: `git push origin vX.Y.Z` — release workflow publishes to PyPI via Trusted Publisher OIDC.
95+
6. Verify on PyPI: `pip install supamem==X.Y.Z` in a clean venv.
96+
97+
PyPI tags are immutable: never re-use a published version number.
98+
99+
## Update-check (v0.1.1+)
100+
101+
A daemon thread probes GitHub Releases on every CLI invocation, caches result for 24h in `platformdirs.user_cache_dir("supamem")/update_check.json`, and prints a stderr footer on the *next* invocation if a newer version is available. Suppress with `SUPAMEM_NO_UPDATE_CHECK=1`, `CI=1`, or `NO_UPDATE_NOTIFIER=1`. Never blocks; never raises.
102+
103+
## Reference Links
104+
105+
- README: high-level overview, install, quickstart
106+
- MIGRATION.md: version-to-version upgrade notes
107+
- CHANGELOG.md: release log
108+
- `docs/` (if present): ADRs, design docs
109+
110+
## Decision Guides
111+
112+
- New backend: register via entry-point group, do NOT modify `cli.py` dispatch
113+
- New CLI subcommand: add Typer command in `cli.py`, route to module under `src/supamem/`
114+
- New config field: extend `config.py` Pydantic schema + bump default in `share/default.toml`
115+
- New hook target: add module under `src/supamem/hooks/<client>.py`, register in `cli.py hook` dispatcher
116+
- Failure in network code: blanket `except Exception: pass` is correct for non-essential probes (update_check); for indexing/retrieval, surface error to user via `err_console`

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
All notable changes to `supamem` will be documented in this file.
44

5+
## v0.1.1 — 2026-04-29
6+
7+
First PyPI release. Hardens v0.1.0 with CI fixes, agent guides, an update-check
8+
notifier, and a published wheel/sdist on PyPI so downstream consumers can pin a
9+
version range instead of a git tag.
10+
11+
### Added
12+
13+
- `supamem.update_check` — pip-style fire-and-forget GitHub Releases probe.
14+
Daemon thread writes `platformdirs.user_cache_dir("supamem")/update_check.json`;
15+
the *next* invocation prints a stderr footer if a newer release is cached.
16+
24h TTL, 6h backoff on 403/429, ETag-aware. Suppress with
17+
`SUPAMEM_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`, or `CI=1`. Skipped when
18+
stderr is non-TTY. Surfaced in `supamem doctor` (new "Update check" section).
19+
- `AGENTS.md` + `CLAUDE.md` — agent-facing project guides per the Apr 2026
20+
cross-tool convention.
21+
22+
### Changed
23+
24+
- License metadata migrated to PEP 639 SPDX expression (`license = "MIT"`),
25+
removing legacy `{ text = "MIT" }` form.
26+
- Distribution channel: PyPI (was git-tag-only). Install via
27+
`pip install supamem` or `uv tool install supamem`.
28+
29+
### Fixed
30+
31+
- `tests/test_cli_smoke.py` subprocess env now pins `NO_COLOR=1`, `TERM=dumb`,
32+
`COLUMNS=200`, and pops `FORCE_COLOR` — eliminates Rich color escapes that
33+
broke CI assertions on GitHub Actions runners.
34+
535
## v0.1.0 — 2026-04-29
636

737
The initial public release. Extracted from the SoftChat dual-memory stack

CLAUDE.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# CLAUDE.md — supamem
2+
3+
Claude Code instructions for the supamem repository.
4+
5+
@AGENTS.md
6+
7+
## Workflow
8+
9+
- Use `/brainstorm` before ambiguous feature work
10+
- Use `/write-plan` before multi-step implementation
11+
- Use `/systematic-debug` before proposing bug fixes
12+
- Verify with `uv run pytest` (project root) and `uv run ruff check src tests` before claiming done
13+
14+
## Hard Constraints
15+
16+
- NEVER use bare `print()` — import from `src/supamem/console.py`
17+
- NEVER print to stdout from `mcp_server.py` — JSON-RPC purity (use `err_console`)
18+
- NEVER hardcode collection names — read `config.collection`
19+
- NEVER skip the PEP 639 license check on release — `license = "MIT"` SPDX, no `License ::` classifier
20+
- NEVER force-move a published git tag — PyPI rejects re-uploads of the same version
21+
- NEVER suppress errors in indexing/retrieval paths — surface via `err_console`
22+
- Update-check is the ONLY code path where blanket `except Exception: pass` is acceptable
23+
24+
## Definition Of Done
25+
26+
- `uv run pytest` green (full suite)
27+
- `uv run ruff check src tests` clean
28+
- New CLI subcommand: smoke test in `tests/test_cli_smoke.py`
29+
- New backend: registered via entry-point, covered by unit + integration test
30+
- Release: `uv build && uvx twine check dist/*` clean before tagging
31+
32+
## Critical Config
33+
34+
- Config discovery order: `.supamem/config.toml``~/.config/supamem/config.toml``share/default.toml`
35+
- Cache dir: `platformdirs.user_cache_dir("supamem")` — never `~/.cache/supamem` directly
36+
- CI env: smoke tests pin `NO_COLOR=1`, `TERM=dumb`, `COLUMNS=200`, pop `FORCE_COLOR`
37+
38+
## When Blocked
39+
40+
- Test fails on CI but passes locally: check Rich color escape pollution (see test_cli_smoke env override)
41+
- Import error: confirm `uv sync --extra dev` ran; check entry-points in `pyproject.toml`
42+
- Qdrant connection failure: `docker compose up qdrant` or use mock fixture
43+
- 2+ verification failures: stop and report blocker with command output

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "supamem"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "Project-agnostic dual-memory tooling for Claude Code, Cursor, and opencode"
99
readme = "README.md"
10-
license = { text = "MIT" }
10+
license = "MIT"
1111
requires-python = ">=3.12"
1212
authors = [{ name = "dzmitrys-dev" }]
1313
dependencies = [
@@ -19,6 +19,8 @@ dependencies = [
1919
"pydantic>=2.5",
2020
"tomli-w>=1.0",
2121
"langchain-text-splitters>=0.3",
22+
"platformdirs>=4.2",
23+
"packaging>=23.0",
2224
]
2325

2426
[project.urls]

src/supamem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""supamem — project-agnostic dual-memory tooling."""
2-
__version__ = "0.1.0"
2+
__version__ = "0.1.1"

src/supamem/cli.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,23 @@ def cmd_migrate(
244244

245245

246246
def main() -> None:
247-
app()
247+
# Fire-and-forget update probe — writes cache for *next* invocation. Skipped
248+
# when env vars opt out, when stderr is non-TTY (piped/redirected output),
249+
# or when the in-process probe fails for any reason. Never blocks.
250+
from supamem.update_check import get_pending_notification, start_background_check
251+
252+
start_background_check(__version__)
253+
try:
254+
app()
255+
finally:
256+
# Print any update notice queued from the *previous* invocation. Goes
257+
# to stderr so JSON consumers piping stdout are unaffected.
258+
notice = get_pending_notification(__version__)
259+
if notice:
260+
try:
261+
err_console.print(notice, end="")
262+
except Exception:
263+
pass
248264

249265

250266
if __name__ == "__main__":

src/supamem/doctor.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import logging
1919
import re
20+
import time
2021
from dataclasses import asdict
2122
from pathlib import Path
2223
from typing import Any
@@ -160,7 +161,32 @@ def run_doctor(*, redact_secrets: bool = True) -> int:
160161
else:
161162
ok(f"{row['client']:<12} v{row['block_version']} (current)")
162163

163-
# ── Section 4: Exit code ─────────────────────────────────────────────
164+
# ── Section 4: Update check ──────────────────────────────────────────
165+
console.print()
166+
console.print("[supamem.brand]Update check[/supamem.brand]")
167+
from supamem.update_check import doctor_report
168+
169+
uc = doctor_report(__version__)
170+
cached = uc.get("cached_latest_version")
171+
last_ts = uc.get("last_check_ts")
172+
last_human = (
173+
time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(last_ts))
174+
if last_ts
175+
else "never"
176+
)
177+
if uc.get("update_available"):
178+
warn(
179+
f"update available: {uc['current_version']}{cached} "
180+
f"(last check: {last_human})"
181+
)
182+
elif cached:
183+
ok(f"on latest cached version v{cached} (last check: {last_human})")
184+
else:
185+
info(f"no probe yet — runs in background on next invocation (cache: {uc['cache_path']})")
186+
if uc.get("suppressed_by_env"):
187+
info(f"suppressed by env: {', '.join(uc['suppressed_by_env'])}")
188+
189+
# ── Section 5: Exit code ─────────────────────────────────────────────
164190
rc = 0
165191
if not qdrant_up or any_drift or (qdrant_up and not coll_status.get("present")):
166192
rc = 1

0 commit comments

Comments
 (0)