poppy-memory 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. poppy_memory-0.2.0/.github/workflows/ci.yml +69 -0
  2. poppy_memory-0.2.0/.github/workflows/publish.yml +36 -0
  3. poppy_memory-0.2.0/.gitignore +29 -0
  4. poppy_memory-0.2.0/CHANGELOG.md +87 -0
  5. poppy_memory-0.2.0/CLAUDE.md +99 -0
  6. poppy_memory-0.2.0/CONTRIBUTING.md +12 -0
  7. poppy_memory-0.2.0/LICENSE +662 -0
  8. poppy_memory-0.2.0/PKG-INFO +269 -0
  9. poppy_memory-0.2.0/README.md +238 -0
  10. poppy_memory-0.2.0/SECURITY.md +17 -0
  11. poppy_memory-0.2.0/benchmarks/BENCHMARKS.md +72 -0
  12. poppy_memory-0.2.0/docs/assets/readme-header-dark.svg +11 -0
  13. poppy_memory-0.2.0/docs/assets/readme-header-light.svg +11 -0
  14. poppy_memory-0.2.0/install.sh +42 -0
  15. poppy_memory-0.2.0/mcpb/icon.png +0 -0
  16. poppy_memory-0.2.0/mcpb/manifest.json +64 -0
  17. poppy_memory-0.2.0/mcpb/server/main.py +46 -0
  18. poppy_memory-0.2.0/pyproject.toml +88 -0
  19. poppy_memory-0.2.0/src/poppy/__init__.py +27 -0
  20. poppy_memory-0.2.0/src/poppy/build_mcpb.py +91 -0
  21. poppy_memory-0.2.0/src/poppy/capture/__init__.py +13 -0
  22. poppy_memory-0.2.0/src/poppy/capture/_state.py +46 -0
  23. poppy_memory-0.2.0/src/poppy/capture/banner.py +92 -0
  24. poppy_memory-0.2.0/src/poppy/capture/cadence.py +71 -0
  25. poppy_memory-0.2.0/src/poppy/capture/journal.py +126 -0
  26. poppy_memory-0.2.0/src/poppy/capture/lock.py +79 -0
  27. poppy_memory-0.2.0/src/poppy/capture/policy.py +163 -0
  28. poppy_memory-0.2.0/src/poppy/capture/reconciler.py +169 -0
  29. poppy_memory-0.2.0/src/poppy/capture/watermark.py +43 -0
  30. poppy_memory-0.2.0/src/poppy/capture/window.py +134 -0
  31. poppy_memory-0.2.0/src/poppy/cli/__init__.py +0 -0
  32. poppy_memory-0.2.0/src/poppy/cli/hooks.py +753 -0
  33. poppy_memory-0.2.0/src/poppy/cli/main.py +1539 -0
  34. poppy_memory-0.2.0/src/poppy/config.py +229 -0
  35. poppy_memory-0.2.0/src/poppy/conflict_detection.py +210 -0
  36. poppy_memory-0.2.0/src/poppy/consolidation.py +565 -0
  37. poppy_memory-0.2.0/src/poppy/db.py +43 -0
  38. poppy_memory-0.2.0/src/poppy/engine/__init__.py +3 -0
  39. poppy_memory-0.2.0/src/poppy/engine/_model_cache.py +55 -0
  40. poppy_memory-0.2.0/src/poppy/engine/_st_loader.py +68 -0
  41. poppy_memory-0.2.0/src/poppy/engine/bloom.py +588 -0
  42. poppy_memory-0.2.0/src/poppy/engine/interface.py +60 -0
  43. poppy_memory-0.2.0/src/poppy/engine/migration.py +273 -0
  44. poppy_memory-0.2.0/src/poppy/engine/registry.py +102 -0
  45. poppy_memory-0.2.0/src/poppy/engine/seed.py +321 -0
  46. poppy_memory-0.2.0/src/poppy/engine/sprout.py +374 -0
  47. poppy_memory-0.2.0/src/poppy/errors.py +14 -0
  48. poppy_memory-0.2.0/src/poppy/integrations/claude_memory_import.py +145 -0
  49. poppy_memory-0.2.0/src/poppy/integrations/hermes_memory_import.py +121 -0
  50. poppy_memory-0.2.0/src/poppy/lifecycle.py +210 -0
  51. poppy_memory-0.2.0/src/poppy/mcp_server/__init__.py +0 -0
  52. poppy_memory-0.2.0/src/poppy/mcp_server/server.py +518 -0
  53. poppy_memory-0.2.0/src/poppy/models.py +38 -0
  54. poppy_memory-0.2.0/src/poppy/runtime.py +75 -0
  55. poppy_memory-0.2.0/src/poppy/setup/__init__.py +0 -0
  56. poppy_memory-0.2.0/src/poppy/setup/claude_code.py +439 -0
  57. poppy_memory-0.2.0/src/poppy/setup/goose.py +184 -0
  58. poppy_memory-0.2.0/src/poppy/setup/hermes.py +558 -0
  59. poppy_memory-0.2.0/src/poppy/setup/trags.py +142 -0
  60. poppy_memory-0.2.0/src/poppy/sources.py +86 -0
  61. poppy_memory-0.2.0/src/poppy/sync/__init__.py +299 -0
  62. poppy_memory-0.2.0/src/poppy/sync/auto.py +226 -0
  63. poppy_memory-0.2.0/src/poppy/sync/client.py +143 -0
  64. poppy_memory-0.2.0/src/poppy/sync/serializer.py +94 -0
  65. poppy_memory-0.2.0/src/poppy/sync/state.py +76 -0
  66. poppy_memory-0.2.0/src/poppy/telemetry.py +217 -0
  67. poppy_memory-0.2.0/src/poppy/ui/__init__.py +6 -0
  68. poppy_memory-0.2.0/src/poppy/ui/server.py +489 -0
  69. poppy_memory-0.2.0/src/poppy/ui/static/app.js +944 -0
  70. poppy_memory-0.2.0/src/poppy/ui/static/fonts/Fraunces-Italic-latin.woff2 +0 -0
  71. poppy_memory-0.2.0/src/poppy/ui/static/fonts/Fraunces-latin.woff2 +0 -0
  72. poppy_memory-0.2.0/src/poppy/ui/static/fonts/JetBrainsMono-Italic-latin.woff2 +0 -0
  73. poppy_memory-0.2.0/src/poppy/ui/static/fonts/JetBrainsMono-latin.woff2 +0 -0
  74. poppy_memory-0.2.0/src/poppy/ui/static/fonts/OFL-Fraunces.txt +93 -0
  75. poppy_memory-0.2.0/src/poppy/ui/static/fonts/OFL-JetBrainsMono.txt +93 -0
  76. poppy_memory-0.2.0/src/poppy/ui/static/index.html +101 -0
  77. poppy_memory-0.2.0/src/poppy/ui/static/styles.css +809 -0
  78. poppy_memory-0.2.0/src/poppy/ui/tombstones.py +147 -0
  79. poppy_memory-0.2.0/tests/capture/__init__.py +0 -0
  80. poppy_memory-0.2.0/tests/capture/test_banner.py +112 -0
  81. poppy_memory-0.2.0/tests/capture/test_cadence.py +52 -0
  82. poppy_memory-0.2.0/tests/capture/test_capture_worker.py +249 -0
  83. poppy_memory-0.2.0/tests/capture/test_consent_cli.py +149 -0
  84. poppy_memory-0.2.0/tests/capture/test_lock.py +39 -0
  85. poppy_memory-0.2.0/tests/capture/test_policy.py +86 -0
  86. poppy_memory-0.2.0/tests/capture/test_reconciler.py +179 -0
  87. poppy_memory-0.2.0/tests/capture/test_sessionend_window.py +126 -0
  88. poppy_memory-0.2.0/tests/capture/test_watermark.py +34 -0
  89. poppy_memory-0.2.0/tests/capture/test_window.py +106 -0
  90. poppy_memory-0.2.0/tests/conftest.py +23 -0
  91. poppy_memory-0.2.0/tests/test_build_mcpb.py +119 -0
  92. poppy_memory-0.2.0/tests/test_claude_memory_import.py +178 -0
  93. poppy_memory-0.2.0/tests/test_cli.py +302 -0
  94. poppy_memory-0.2.0/tests/test_cli_since.py +218 -0
  95. poppy_memory-0.2.0/tests/test_cli_telemetry.py +173 -0
  96. poppy_memory-0.2.0/tests/test_config.py +136 -0
  97. poppy_memory-0.2.0/tests/test_conflict_detection.py +265 -0
  98. poppy_memory-0.2.0/tests/test_consolidation.py +167 -0
  99. poppy_memory-0.2.0/tests/test_db.py +41 -0
  100. poppy_memory-0.2.0/tests/test_engine_bloom.py +248 -0
  101. poppy_memory-0.2.0/tests/test_engine_migration.py +265 -0
  102. poppy_memory-0.2.0/tests/test_engine_registry.py +153 -0
  103. poppy_memory-0.2.0/tests/test_engine_seed.py +141 -0
  104. poppy_memory-0.2.0/tests/test_engine_sprout.py +156 -0
  105. poppy_memory-0.2.0/tests/test_engine_st_loader.py +117 -0
  106. poppy_memory-0.2.0/tests/test_hooks.py +571 -0
  107. poppy_memory-0.2.0/tests/test_lifecycle.py +274 -0
  108. poppy_memory-0.2.0/tests/test_mcp_server.py +303 -0
  109. poppy_memory-0.2.0/tests/test_models.py +69 -0
  110. poppy_memory-0.2.0/tests/test_setup_claude_code.py +309 -0
  111. poppy_memory-0.2.0/tests/test_setup_goose.py +171 -0
  112. poppy_memory-0.2.0/tests/test_setup_hermes.py +185 -0
  113. poppy_memory-0.2.0/tests/test_setup_trags.py +53 -0
  114. poppy_memory-0.2.0/tests/test_sources.py +97 -0
  115. poppy_memory-0.2.0/tests/test_telemetry.py +232 -0
  116. poppy_memory-0.2.0/tests/test_ui_lifecycle.py +220 -0
  117. poppy_memory-0.2.0/tests/test_version.py +57 -0
@@ -0,0 +1,69 @@
1
+ name: CI
2
+
3
+ # push is restricted to main so PR branches run once (via pull_request),
4
+ # not twice (push + pull_request) — PR #3 review follow-up.
5
+ # pull_request filters on the BASE branch, so stacked PRs (base reveal-prep/*)
6
+ # must be listed too or they would get zero CI runs.
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+ branches:
12
+ - main
13
+ - "reveal-prep/**"
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ matrix:
20
+ python-version: ["3.11", "3.12", "3.13"]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v4
27
+
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ - name: Install dependencies
32
+ run: uv sync
33
+
34
+ - name: Lint
35
+ run: |
36
+ uv run ruff check src/ tests/
37
+ uv run ruff format --check src/ tests/
38
+
39
+ - name: Test
40
+ run: uv run pytest -v
41
+
42
+ # Build-only release rehearsal on every push. Publishing to PyPI happens
43
+ # exclusively in publish.yml, which triggers only on v* tags.
44
+ build:
45
+ runs-on: ubuntu-latest
46
+
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - name: Set up Python
51
+ uses: actions/setup-python@v5
52
+ with:
53
+ python-version: "3.13"
54
+
55
+ - name: Install build tooling
56
+ run: python -m pip install --upgrade pip build twine
57
+
58
+ - name: Build wheel + sdist
59
+ run: python -m build
60
+
61
+ - name: Verify build metadata
62
+ run: python -m twine check dist/*
63
+
64
+ - name: Upload dist artifacts
65
+ uses: actions/upload-artifact@v4
66
+ with:
67
+ name: dist
68
+ path: dist/
69
+ retention-days: 7
@@ -0,0 +1,36 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-and-publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.13"
22
+
23
+ - name: Install build tooling
24
+ run: python -m pip install --upgrade pip build twine
25
+
26
+ - name: Build sdist + wheel
27
+ run: python -m build
28
+
29
+ - name: Verify build metadata
30
+ run: python -m twine check dist/*
31
+
32
+ - name: Publish to PyPI
33
+ env:
34
+ TWINE_USERNAME: __token__
35
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
36
+ run: python -m twine upload dist/*
@@ -0,0 +1,29 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ .uv-cache/
10
+ uv.lock
11
+
12
+ # Editor / IDE
13
+ .vscode/
14
+ .idea/
15
+ *.swp
16
+
17
+ # OS
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Pytest / coverage
22
+ .pytest_cache/
23
+ .coverage
24
+ htmlcov/
25
+ .tox/
26
+
27
+ # Built .mcpb bundles (the mcpb/ source dir with manifest.json + shim is checked in)
28
+ *.mcpb
29
+ mcpb-build/
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to Poppy are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-06-14
9
+
10
+ First public release of `poppy-memory`.
11
+
12
+ ### Added
13
+
14
+ - Core memory CLI: `remember`, `recall`, `list`, `edit`, `forget`, `expire`,
15
+ `stats`, with types (fact, decision, preference, lesson), projects, TTLs,
16
+ supersede chains, and LLM conflict detection (`--check-conflicts`,
17
+ `--auto-supersede`).
18
+ - Three retrieval engines with a floating default tier: `bloom` (hybrid FTS5 +
19
+ bge-small embeddings + cross-encoder rerank), `sprout` (lighter bi-encoder),
20
+ `seed` (FTS5 only, zero ML deps). Switch with `poppy engines use`, re-embed
21
+ with `poppy migrate-engine`.
22
+ - MCP server (`poppy serve`) plus one-command setup for Claude Code (MCP,
23
+ lifecycle hooks, CLAUDE.md primer), Claude Desktop, Cursor, Windsurf, Codex,
24
+ Copilot CLI, Pi, Goose, and Hermes Agent. `poppy doctor` verifies the whole
25
+ installation.
26
+ - Claude Desktop Extension bundle builder (`poppy build mcpb`); the build
27
+ stamps the pyproject version into the bundle manifest.
28
+ - Optional Trags cloud sync: device-code onboarding (`poppy setup trags`),
29
+ `poppy sync push|pull|run|status`, and auto-sync after local writes.
30
+ - Imports from existing stores: `poppy import claude-memories` and
31
+ `poppy import hermes-memories`.
32
+ - Local web UI (`poppy ui`) for browsing, editing, and restoring memories.
33
+ - `--since` filter on `recall`/`list` accepting ISO dates or durations such as
34
+ `7d` and `1w3d`.
35
+ - Telemetry disclosure and first-class opt-out: `poppy telemetry
36
+ status|on|off` persisting to `~/.poppy/config.json`, a one-time first-run
37
+ notice on stderr, and a README section listing every event and property
38
+ sent. `POPPY_TELEMETRY_OFF=1` always wins.
39
+ - `poppy --version`, single-sourced from `pyproject.toml`.
40
+ - Zero-touch capture: consent-gated automatic memory extraction from coding
41
+ sessions. Mid-session captures fire every Nth prompt under a single-flight
42
+ lock with a per-session soft cap; a SessionEnd backstop and PostCompact
43
+ re-extraction cover whatever the cadence missed. An incremental transcript
44
+ window (watermark-based, compaction-safe) feeds extraction, and a
45
+ dedup-on-capture reconciler decides ADD / SUPERSEDE / SKIP before anything
46
+ is written. Extraction runs locally through the user's own host CLI
47
+ (claude/codex/gemini); a remote-only backend is never auto-spent.
48
+ - `poppy consent --enable/--disable/--status` plus a one-time consent prompt
49
+ in `poppy setup claude-code` (and a `--yes` flag): nothing is captured
50
+ until consent is granted, and an opt-out persists.
51
+ - SessionStart status banner (active / inactive / consent pending) and
52
+ expanded `poppy doctor` capture reporting: granular consent and backend
53
+ status, last-capture freshness, journal count, and watermark/lock state.
54
+ - Source-app provenance on MCP writes: memories record the real client app
55
+ (for example `claude-code` or `cursor`) from `poppy serve --source` or the
56
+ connecting client's `clientInfo`, never the bare transport string `mcp`.
57
+ - Legacy engine names in an existing `config.json` keep working:
58
+ `speaker_closet` and `best` map silently to `bloom`, `baseline` to `seed`.
59
+ - `since` filter on the MCP `recall` tool, accepting the same values as the
60
+ CLI `--since` (ISO date or a duration such as `7d`).
61
+ - Offline-safe retrieval model loading: already-downloaded models load with
62
+ `local_files_only`, so recall keeps working with the network down; a cold
63
+ cache with no network exits with a one-line actionable error instead of a
64
+ HuggingFace traceback; a one-time stderr notice announces the first-run
65
+ model download.
66
+
67
+ ### Fixed
68
+
69
+ - `--since` on `recall`/`list` was accepted but silently ignored; it now
70
+ filters and rejects invalid values with a usage error.
71
+ - `recall`/`list` no longer print the misleading "No memories found." when
72
+ memories exist but active `--since`/`--project`/`--type` filters excluded
73
+ them; they say the filters matched nothing instead.
74
+ - Recall telemetry reported a stale hardcoded engine name instead of the
75
+ engine that actually served the query.
76
+ - The `memory_write` telemetry event sent the raw project name; it now sends
77
+ only a `has_project` boolean.
78
+
79
+ ### Security
80
+
81
+ - License set to AGPL-3.0-or-later.
82
+ - `~/.poppy/config.json` (holds API keys) is written 0600 inside a 0700
83
+ directory, atomically, so secrets never land world-readable.
84
+ - The `poppy setup trags` device-code flow encrypts the returned API key to an
85
+ ephemeral RSA keypair, so the key never crosses the wire in plaintext.
86
+ - Runtime SQLite connections enable WAL and a busy timeout, preventing
87
+ lock-related corruption under concurrent hook/CLI access.
@@ -0,0 +1,99 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ **Poppy** is a developer memory CLI and MCP server. "Remember what matters."
8
+
9
+ Agents store and recall memories via MCP tools; humans browse via the CLI and the
10
+ local web UI. Data lives locally at `~/.poppy/` (SQLite + FTS5 + embeddings).
11
+ This is the publish repo for the `poppy-memory` package; experimental engine work
12
+ lives elsewhere and is promoted here when it wins.
13
+
14
+ ## Tech Stack
15
+
16
+ - **Language**: Python 3.11+
17
+ - **Package manager**: uv
18
+ - **CLI**: click
19
+ - **MCP server**: mcp SDK (FastMCP)
20
+ - **Storage**: SQLite + FTS5; embedding engines use sentence-transformers
21
+ - **Testing**: pytest + pytest-asyncio
22
+
23
+ ## Development
24
+
25
+ ```bash
26
+ uv sync # install dependencies
27
+ uv run pytest -v # run tests
28
+ uv run ruff check src/ tests/
29
+ uv run ruff format src/ tests/
30
+ uv run poppy --help # CLI help
31
+ ```
32
+
33
+ Use the `POPPY_DIR` env var to avoid touching `~/.poppy/` during development.
34
+ The test suite isolates this automatically (see `tests/conftest.py`).
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ src/poppy/
40
+ ├── models.py # Memory, Source, Filters dataclasses
41
+ ├── config.py # Config load/save, consent tri-state, legacy migration
42
+ ├── sources.py # Source-app provenance vocabulary
43
+ ├── engine/
44
+ │ ├── interface.py # RetrievalEngine ABC (the stable boundary)
45
+ │ ├── registry.py # Engine catalog + legacy-name fallback mapping
46
+ │ ├── bloom.py # Default: hybrid FTS5+embeddings, cross-encoder rerank
47
+ │ ├── sprout.py # Mid tier: lighter bi-encoder, same architecture
48
+ │ └── seed.py # FTS5 only, no ML deps (universal fallback)
49
+ ├── capture/ # Zero-touch capture stack (see below)
50
+ ├── consolidation.py # LLM extraction: backstop, mid-session, post-compact
51
+ ├── cli/
52
+ │ ├── main.py # click CLI commands (incl. consent, doctor)
53
+ │ └── hooks.py # Claude Code hook entrypoints + detached workers
54
+ ├── mcp_server/ # FastMCP server + tool handlers
55
+ ├── setup/ # Per-client install (claude-code, cursor, goose, ...)
56
+ ├── sync/ # Optional Trags cloud sync
57
+ └── ui/ # Local web UI
58
+ ```
59
+
60
+ ### Zero-touch capture (`src/poppy/capture/`)
61
+
62
+ Automatic memory extraction from coding sessions. Consent-gated: enabled by
63
+ default but inert until `poppy consent --enable` (ADR-0002).
64
+
65
+ | Module | Role |
66
+ |---|---|
67
+ | `policy.py` | ConsolidationPolicy: consent tri-state + backend detection. Single source of truth for "is capture on, and if not, why". |
68
+ | `_state.py` / `watermark.py` | Per-session JSON state; the watermark marks the last captured turn so no turn is extracted twice. Reset at SessionStart. |
69
+ | `cadence.py` | Fire every Nth user turn; per-session soft cap defers the rest to the SessionEnd backstop. |
70
+ | `lock.py` | Single-flight per-session lock; stale locks (crashed workers) are stolen after 5 minutes. |
71
+ | `window.py` | Incremental transcript reader for `(watermark, now]`; survives compaction by clamping. |
72
+ | `reconciler.py` | Dedup-on-capture before ingest: ADD / SUPERSEDE / SKIP, three tiers (lexical prefilter, LLM verdict, bias to ADD). |
73
+ | `journal.py` | Local JSONL record of what each capture stored. Never synced. |
74
+ | `banner.py` | SessionStart status banner (active / INACTIVE / consent pending). |
75
+
76
+ Flow: `UserPromptSubmit` hook counts turns and, on cadence, spawns a detached
77
+ `poppy hook _capture-worker` which runs `consolidation.consolidate_capture_event`
78
+ (lock, window, gates, LLM extract, reconcile, watermark advance, journal).
79
+ `SessionEnd` and `PostCompact` run the same extraction as backstops. Extraction
80
+ uses the host CLI (`claude` / `codex` / `gemini`) so it is free and local; a
81
+ remote-only backend is never auto-spent.
82
+
83
+ Capture state files (`capture_state.json`, `capture_journal.jsonl`,
84
+ `capture-*.lock`) are per-device by design and excluded from sync.
85
+
86
+ ### Key boundary: RetrievalEngine ABC
87
+
88
+ Everything above `RetrievalEngine` is stable. New engines implement the ABC and
89
+ register in `engine/registry.py`. `interface.py` does not change without
90
+ discussion.
91
+
92
+ ## Conventions
93
+
94
+ - Python 3.11+, formatted with ruff (line-length 120)
95
+ - Tests with pytest; async tests with pytest-asyncio
96
+ - Commits: conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, `ci:`, `style:`)
97
+ - User-facing copy: no em dashes
98
+ - Releases are tag-driven (`v*` runs publish.yml); CI builds the wheel on every
99
+ push but never publishes
@@ -0,0 +1,12 @@
1
+ # Contributing
2
+
3
+ Poppy's source is published openly under the GNU Affero General Public License v3
4
+ (see [LICENSE](LICENSE)). You are free to read, run, self-host, fork, and modify
5
+ it under those terms.
6
+
7
+ We do not accept external pull requests. Poppy is developed in-house, and keeping
8
+ a single copyright holder lets us relicense or dual-license cleanly as the project
9
+ evolves. Bug reports are welcome via issues; code contributions will be politely
10
+ declined.
11
+
12
+ If you build something on top of Poppy, we would love to hear about it.